-
module ApplicationCable
-
class Channel < ActionCable::Channel::Base
-
end
-
end
-
module ApplicationCable
-
class Connection < ActionCable::Connection::Base
-
end
-
end
-
require "#{Rails.root}/app/controllers/application_controller.rb"
-
-
module Api
-
module V1
-
class CategoriesController < ApplicationController
-
before_action :set_category, only: [:show, :update, :destroy]
-
def index
-
categories = Category.all
-
render json: categories, status: :created and return
-
end
-
-
def create
-
binding.pry
-
unless Category.json_to_relation(params[:category][:data].as_json)
-
render json: category, status: :created and return
-
else
-
render json: category.errors, status: :unprocessable_entity
-
end
-
end
-
-
def update
-
if category.update(category_params)
-
render json: category and return
-
else
-
render json: category.errors, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def set_category
-
problem = Problem.find(params[:id])
-
end
-
-
def category_params
-
params.require(:category).permit(:name)
-
end
-
end
-
end
-
end
-
require "#{Rails.root}/app/controllers/application_controller.rb"
-
-
module Api
-
module V1
-
class ProblemsController < ApplicationController
-
before_action :set_problem, only: [:show, :update, :destroy]
-
def index
-
problems = Problem.all.order(id:'desc')
-
render json: problems, status: :ok and return
-
end
-
-
def questions
-
questions = Problem.of_questioin_select
-
render json: questions, status: :ok and return
-
end
-
def create
-
problem = Problem.new(problem_params)
-
-
if problem.save!
-
render json: problem, status: :created and return
-
else
-
render json: problem.errors, status: :unprocessable_entity
-
end
-
end
-
-
def update
-
if problem.update(problem_params)
-
render json: problem, status: :created and return
-
else
-
render json: problem.errors, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def set_problem
-
problem = Problem.find(params[:id])
-
end
-
-
def problem_params
-
params.require(:problem).permit(:body,:question_at,:period_id,:format_id,{categories:[]})
-
end
-
end
-
end
-
end
-
class ApplicationController < ActionController::API
-
end
-
class ApplicationJob < ActiveJob::Base
-
# Automatically retry jobs that encountered a deadlock
-
# retry_on ActiveRecord::Deadlocked
-
-
# Most jobs are safe to ignore if the underlying records are no longer available
-
# discard_on ActiveJob::DeserializationError
-
end
-
class ApplicationMailer < ActionMailer::Base
-
default from: 'from@example.com'
-
layout 'mailer'
-
end
-
class ApplicationRecord < ActiveRecord::Base
-
self.abstract_class = true
-
end
-
class Author < ApplicationRecord
-
validates :name,
-
presence: true
-
-
has_and_belongs_to_many :books
-
end
-
class AuthorsBook < ApplicationRecord
-
end
-
class Book < ApplicationRecord
-
validates :price,
-
presence: true
-
validates :categories,
-
presence: true
-
validates :published_at,
-
presence: true
-
-
has_and_belongs_to_many :problems
-
has_and_belongs_to_many :authors
-
end
-
class BooksProblem < ApplicationRecord
-
end
-
1
class Category < ApplicationRecord
-
1
validates :title,
-
presence: true
-
1
validates :key,
-
presence: true
-
1
validates :group,
-
presence: true
-
-
1
has_and_belongs_to_many :books
-
1
has_many :category_relationships
-
1
has_many :children, through: :category_relationships, source: :child
-
1
has_many :reverses_of_relation, class_name: 'CategoryRelationship', foreign_key: 'child_id'
-
1
has_many :parents, through: :reverses_of_relation, source: :category
-
-
#function
-
1
def self.json_to_relation(json_data,root_id=-1)
-
3
return false if json_data.blank?
-
3
nodes = []
-
# binding.pry
-
3
json_data.each do |node|
-
4
record = {title:node["title"],key:node["key"]}
-
4
if node["key"] == '0-0'
-
1
record[:root] = true
-
1
record[:group] = -1
-
1
category = Category.new(record)
-
1
raise Exception.new("root node save error") unless category.save
-
1
if node.has_key?("children")
-
1
children = json_to_relation(node["children"],root_id)
-
1
record[:group] = root_id
-
1
category = Category.new(record)
-
1
raise Exception.new("children save error") unless category.save
-
1
children.each do |node_id|
-
1
category.category_relationships.find_or_create_by(child_id: node_id)
-
end
-
end
-
3
elsif node.has_key?("children")
-
1
children = json_to_relation(node["children"],root_id)
-
1
record[:group] = root_id
-
1
category = Category.new(record)
-
1
raise Exception.new("children save error") unless category.save
-
1
children.each do |node_id|
-
2
category.category_relationships.find_or_create_by(child_id: node_id)
-
end
-
else
-
2
record[:group] = root_id
-
2
category = Category.new(record)
-
2
raise Exception.new("leaf node save error") unless category.save!
-
end
-
4
nodes.push(category.id)
-
end
-
3
nodes
-
end
-
end
-
class CategoryRelationship < ApplicationRecord
-
belongs_to :category
-
belongs_to :child, class_name: 'Category'
-
end
-
class Format < ApplicationRecord
-
validates :problem_type,
-
presence: true
-
enum type: {
-
sentence_problem: 0,
-
blank_problem: 1,
-
image_problem: 2,
-
reordering_problem: 3
-
}
-
-
has_many :problems
-
end
-
class Period < ApplicationRecord
-
validates :period,
-
presence: true
-
-
has_many :problems
-
-
#function
-
def next
-
Period.find_by(id: self.id+1) == nil ? self.period : Period.find_by(id: self.id+1).period
-
end
-
-
def next_period_id
-
Period.find_by(id: self.id+1) == nil ? self.id : self.id+1
-
end
-
-
def prev
-
self.id-1 < 0 ? self.period : Period.find_by(id: self.id-1).period
-
end
-
-
def prev_period_id
-
self.id < 1 ? self.id : self.id-1
-
end
-
end
-
class Problem < ApplicationRecord
-
validates :body,
-
presence: true
-
validates :categories,
-
presence: true
-
-
belongs_to :format
-
belongs_to :period
-
has_and_belongs_to_many :books
-
-
# function
-
def self.of_questioin_select
-
Problem.where(question_at:(Date.current.beginning_of_day)..(Date.current.end_of_day))
-
end
-
-
def calc_question_at(correct_wrong, answer_speed)
-
if answer_speed < 600
-
if correct_wrong
-
self.question_at += self.period.next.days
-
self.period_id = self.period.next_period_id
-
else
-
self.question_at += self.period.prev.days
-
self.period_id = self.period.prev_period_id
-
end
-
else
-
self.question_at += self.period.period.days
-
self.period_id = self.period.prev_period_id
-
end
-
self.save
-
end
-
end
-
#unicornのpidファイル、設定ファイルのディレクトリを指定
-
namespace :unicorn do
-
task :environment do
-
set :unicorn_pid, "#{current_path}/tmp/pids/unicorn.pid"
-
set :unicorn_config, "#{current_path}/config/unicorn/production.rb"
-
end
-
-
#unicornをスタートさせるメソッド
-
def start_unicorn
-
within current_path do
-
execute :bundle, :exec, :unicorn, "-c #{fetch(:unicorn_config)} -E #{fetch(:rails_env)} -D"
-
end
-
end
-
-
#unicornを停止させるメソッド
-
def stop_unicorn
-
execute :kill, "-s QUIT $(< #{fetch(:unicorn_pid)})"
-
end
-
-
#unicornを再起動するメソッド
-
def reload_unicorn
-
execute :kill, "-s USR2 $(< #{fetch(:unicorn_pid)})"
-
end
-
-
#unicronを強制終了するメソッド
-
def force_stop_unicorn
-
execute :kill, "$(< #{fetch(:unicorn_pid)})"
-
end
-
-
#unicornをスタートさせるtask
-
desc "Start unicorn server"
-
task start: :environment do
-
on roles(:app) do
-
start_unicorn
-
end
-
end
-
-
#unicornを停止させるtask
-
desc "Stop unicorn server gracefully"
-
task stop: :environment do
-
on roles(:app) do
-
stop_unicorn
-
end
-
end
-
-
#既にunicornが起動している場合再起動を、まだの場合起動を行うtask
-
desc "Restart unicorn server gracefully"
-
task restart: :environment do
-
on roles(:app) do
-
if test("[ -f #{fetch(:unicorn_pid)} ]")
-
reload_unicorn
-
else
-
start_unicorn
-
end
-
end
-
end
-
-
#unicornを強制終了させるtask
-
desc "Stop unicorn server immediately"
-
task force_stop: :environment do
-
on roles(:app) do
-
force_stop_unicorn
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support"
-
1
require "active_support/test_case"
-
1
require "active_support/core_ext/hash/indifferent_access"
-
1
require "json"
-
-
1
module ActionCable
-
1
module Channel
-
1
class NonInferrableChannelError < ::StandardError
-
1
def initialize(name)
-
super "Unable to determine the channel to test from #{name}. " +
-
"You'll need to specify it using `tests YourChannel` in your " +
-
"test case definition."
-
end
-
end
-
-
# Stub +stream_from+ to track streams for the channel.
-
# Add public aliases for +subscription_confirmation_sent?+ and
-
# +subscription_rejected?+.
-
1
module ChannelStub
-
1
def confirmed?
-
subscription_confirmation_sent?
-
end
-
-
1
def rejected?
-
subscription_rejected?
-
end
-
-
1
def stream_from(broadcasting, *)
-
streams << broadcasting
-
end
-
-
1
def stop_all_streams
-
@_streams = []
-
end
-
-
1
def streams
-
@_streams ||= []
-
end
-
-
# Make periodic timers no-op
-
1
def start_periodic_timers; end
-
1
alias stop_periodic_timers start_periodic_timers
-
end
-
-
1
class ConnectionStub
-
1
attr_reader :transmissions, :identifiers, :subscriptions, :logger
-
-
1
def initialize(identifiers = {})
-
@transmissions = []
-
-
identifiers.each do |identifier, val|
-
define_singleton_method(identifier) { val }
-
end
-
-
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
-
@identifiers = identifiers.keys
-
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
-
end
-
-
1
def transmit(cable_message)
-
transmissions << cable_message.with_indifferent_access
-
end
-
end
-
-
# Superclass for Action Cable channel functional tests.
-
#
-
# == Basic example
-
#
-
# Functional tests are written as follows:
-
# 1. First, one uses the +subscribe+ method to simulate subscription creation.
-
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
-
# transmitted messages, subscribed streams, etc.
-
#
-
# For example:
-
#
-
# class ChatChannelTest < ActionCable::Channel::TestCase
-
# def test_subscribed_with_room_number
-
# # Simulate a subscription creation
-
# subscribe room_number: 1
-
#
-
# # Asserts that the subscription was successfully created
-
# assert subscription.confirmed?
-
#
-
# # Asserts that the channel subscribes connection to a stream
-
# assert_has_stream "chat_1"
-
#
-
# # Asserts that the channel subscribes connection to a specific
-
# # stream created for a model
-
# assert_has_stream_for Room.find(1)
-
# end
-
#
-
# def test_does_not_stream_with_incorrect_room_number
-
# subscribe room_number: -1
-
#
-
# # Asserts that not streams was started
-
# assert_no_streams
-
# end
-
#
-
# def test_does_not_subscribe_without_room_number
-
# subscribe
-
#
-
# # Asserts that the subscription was rejected
-
# assert subscription.rejected?
-
# end
-
# end
-
#
-
# You can also perform actions:
-
# def test_perform_speak
-
# subscribe room_number: 1
-
#
-
# perform :speak, message: "Hello, Rails!"
-
#
-
# assert_equal "Hello, Rails!", transmissions.last["text"]
-
# end
-
#
-
# == Special methods
-
#
-
# ActionCable::Channel::TestCase will also automatically provide the following instance
-
# methods for use in the tests:
-
#
-
# <b>connection</b>::
-
# An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
-
# <b>subscription</b>::
-
# An instance of the current channel, created when you call +subscribe+.
-
# <b>transmissions</b>::
-
# A list of all messages that have been transmitted into the channel.
-
#
-
#
-
# == Channel is automatically inferred
-
#
-
# ActionCable::Channel::TestCase will automatically infer the channel under test
-
# from the test class name. If the channel cannot be inferred from the test
-
# class name, you can explicitly set it with +tests+.
-
#
-
# class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
-
# tests SpecialChannel
-
# end
-
#
-
# == Specifying connection identifiers
-
#
-
# You need to set up your connection manually to provide values for the identifiers.
-
# To do this just use:
-
#
-
# stub_connection(user: users(:john))
-
#
-
# == Testing broadcasting
-
#
-
# ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g.
-
# +assert_broadcasts+) to handle broadcasting to models:
-
#
-
#
-
# # in your channel
-
# def speak(data)
-
# broadcast_to room, text: data["message"]
-
# end
-
#
-
# def test_speak
-
# subscribe room_id: rooms(:chat).id
-
#
-
# assert_broadcast_on(rooms(:chat), text: "Hello, Rails!") do
-
# perform :speak, message: "Hello, Rails!"
-
# end
-
# end
-
1
class TestCase < ActiveSupport::TestCase
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
-
1
include ActiveSupport::Testing::ConstantLookup
-
1
include ActionCable::TestHelper
-
-
1
CHANNEL_IDENTIFIER = "test_stub"
-
-
1
included do
-
1
class_attribute :_channel_class
-
-
1
attr_reader :connection, :subscription
-
-
1
ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
-
end
-
-
1
module ClassMethods
-
1
def tests(channel)
-
case channel
-
when String, Symbol
-
self._channel_class = channel.to_s.camelize.constantize
-
when Module
-
self._channel_class = channel
-
else
-
raise NonInferrableChannelError.new(channel)
-
end
-
end
-
-
1
def channel_class
-
if channel = self._channel_class
-
channel
-
else
-
tests determine_default_channel(name)
-
end
-
end
-
-
1
def determine_default_channel(name)
-
channel = determine_constant_from_test_name(name) do |constant|
-
Class === constant && constant < ActionCable::Channel::Base
-
end
-
raise NonInferrableChannelError.new(name) if channel.nil?
-
channel
-
end
-
end
-
-
# Set up test connection with the specified identifiers:
-
#
-
# class ApplicationCable < ActionCable::Connection::Base
-
# identified_by :user, :token
-
# end
-
#
-
# stub_connection(user: users[:john], token: 'my-secret-token')
-
1
def stub_connection(identifiers = {})
-
@connection = ConnectionStub.new(identifiers)
-
end
-
-
# Subscribe to the channel under test. Optionally pass subscription parameters as a Hash.
-
1
def subscribe(params = {})
-
@connection ||= stub_connection
-
@subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
-
@subscription.singleton_class.include(ChannelStub)
-
@subscription.subscribe_to_channel
-
@subscription
-
end
-
-
# Unsubscribe the subscription under test.
-
1
def unsubscribe
-
check_subscribed!
-
subscription.unsubscribe_from_channel
-
end
-
-
# Perform action on a channel.
-
#
-
# NOTE: Must be subscribed.
-
1
def perform(action, data = {})
-
check_subscribed!
-
subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
-
end
-
-
# Returns messages transmitted into channel
-
1
def transmissions
-
# Return only directly sent message (via #transmit)
-
connection.transmissions.map { |data| data["message"] }.compact
-
end
-
-
# Enhance TestHelper assertions to handle non-String
-
# broadcastings
-
1
def assert_broadcasts(stream_or_object, *args)
-
super(broadcasting_for(stream_or_object), *args)
-
end
-
-
1
def assert_broadcast_on(stream_or_object, *args)
-
super(broadcasting_for(stream_or_object), *args)
-
end
-
-
# Asserts that no streams have been started.
-
#
-
# def test_assert_no_started_stream
-
# subscribe
-
# assert_no_streams
-
# end
-
#
-
1
def assert_no_streams
-
assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found"
-
end
-
-
# Asserts that the specified stream has been started.
-
#
-
# def test_assert_started_stream
-
# subscribe
-
# assert_has_stream 'messages'
-
# end
-
#
-
1
def assert_has_stream(stream)
-
assert subscription.streams.include?(stream), "Stream #{stream} has not been started"
-
end
-
-
# Asserts that the specified stream for a model has started.
-
#
-
# def test_assert_started_stream_for
-
# subscribe id: 42
-
# assert_has_stream_for User.find(42)
-
# end
-
#
-
1
def assert_has_stream_for(object)
-
assert_has_stream(broadcasting_for(object))
-
end
-
-
1
private
-
1
def check_subscribed!
-
raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
-
end
-
-
1
def broadcasting_for(stream_or_object)
-
return stream_or_object if stream_or_object.is_a?(String)
-
-
self.class.channel_class.broadcasting_for(stream_or_object)
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Connection
-
1
extend ActiveSupport::Autoload
-
-
1
eager_autoload do
-
1
autoload :Authorization
-
1
autoload :Base
-
1
autoload :ClientSocket
-
1
autoload :Identification
-
1
autoload :InternalChannel
-
1
autoload :MessageBuffer
-
1
autoload :Stream
-
1
autoload :StreamEventLoop
-
1
autoload :Subscriptions
-
1
autoload :TaggedLoggerProxy
-
1
autoload :TestCase
-
1
autoload :WebSocket
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support"
-
1
require "active_support/test_case"
-
1
require "active_support/core_ext/hash/indifferent_access"
-
1
require "action_dispatch"
-
1
require "action_dispatch/http/headers"
-
1
require "action_dispatch/testing/test_request"
-
-
1
module ActionCable
-
1
module Connection
-
1
class NonInferrableConnectionError < ::StandardError
-
1
def initialize(name)
-
super "Unable to determine the connection to test from #{name}. " +
-
"You'll need to specify it using `tests YourConnection` in your " +
-
"test case definition."
-
end
-
end
-
-
1
module Assertions
-
# Asserts that the connection is rejected (via +reject_unauthorized_connection+).
-
#
-
# # Asserts that connection without user_id fails
-
# assert_reject_connection { connect params: { user_id: '' } }
-
1
def assert_reject_connection(&block)
-
assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
-
end
-
end
-
-
# We don't want to use the whole "encryption stack" for connection
-
# unit-tests, but we want to make sure that users test against the correct types
-
# of cookies (i.e. signed or encrypted or plain)
-
1
class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
-
1
def signed
-
self[:signed] ||= {}.with_indifferent_access
-
end
-
-
1
def encrypted
-
self[:encrypted] ||= {}.with_indifferent_access
-
end
-
end
-
-
1
class TestRequest < ActionDispatch::TestRequest
-
1
attr_accessor :session, :cookie_jar
-
end
-
-
1
module TestConnection
-
1
attr_reader :logger, :request
-
-
1
def initialize(request)
-
inner_logger = ActiveSupport::Logger.new(StringIO.new)
-
tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
-
@logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
-
@request = request
-
@env = request.env
-
end
-
end
-
-
# Unit test Action Cable connections.
-
#
-
# Useful to check whether a connection's +identified_by+ gets assigned properly
-
# and that any improper connection requests are rejected.
-
#
-
# == Basic example
-
#
-
# Unit tests are written as follows:
-
#
-
# 1. Simulate a connection attempt by calling +connect+.
-
# 2. Assert state, e.g. identifiers, has been assigned.
-
#
-
#
-
# class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
-
# def test_connects_with_proper_cookie
-
# # Simulate the connection request with a cookie.
-
# cookies["user_id"] = users(:john).id
-
#
-
# connect
-
#
-
# # Assert the connection identifier matches the fixture.
-
# assert_equal users(:john).id, connection.user.id
-
# end
-
#
-
# def test_rejects_connection_without_proper_cookie
-
# assert_reject_connection { connect }
-
# end
-
# end
-
#
-
# +connect+ accepts additional information about the HTTP request with the
-
# +params+, +headers+, +session+ and Rack +env+ options.
-
#
-
# def test_connect_with_headers_and_query_string
-
# connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
-
#
-
# assert_equal "1", connection.user.id
-
# assert_equal "secret-my", connection.token
-
# end
-
#
-
# def test_connect_with_params
-
# connect params: { user_id: 1 }
-
#
-
# assert_equal "1", connection.user.id
-
# end
-
#
-
# You can also set up the correct cookies before the connection request:
-
#
-
# def test_connect_with_cookies
-
# # Plain cookies:
-
# cookies["user_id"] = 1
-
#
-
# # Or signed/encrypted:
-
# # cookies.signed["user_id"] = 1
-
# # cookies.encrypted["user_id"] = 1
-
#
-
# connect
-
#
-
# assert_equal "1", connection.user_id
-
# end
-
#
-
# == Connection is automatically inferred
-
#
-
# ActionCable::Connection::TestCase will automatically infer the connection under test
-
# from the test class name. If the channel cannot be inferred from the test
-
# class name, you can explicitly set it with +tests+.
-
#
-
# class ConnectionTest < ActionCable::Connection::TestCase
-
# tests ApplicationCable::Connection
-
# end
-
#
-
1
class TestCase < ActiveSupport::TestCase
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
-
1
DEFAULT_PATH = "/cable"
-
-
1
include ActiveSupport::Testing::ConstantLookup
-
1
include Assertions
-
-
1
included do
-
1
class_attribute :_connection_class
-
-
1
attr_reader :connection
-
-
1
ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
-
end
-
-
1
module ClassMethods
-
1
def tests(connection)
-
case connection
-
when String, Symbol
-
self._connection_class = connection.to_s.camelize.constantize
-
when Module
-
self._connection_class = connection
-
else
-
raise NonInferrableConnectionError.new(connection)
-
end
-
end
-
-
1
def connection_class
-
if connection = self._connection_class
-
connection
-
else
-
tests determine_default_connection(name)
-
end
-
end
-
-
1
def determine_default_connection(name)
-
connection = determine_constant_from_test_name(name) do |constant|
-
Class === constant && constant < ActionCable::Connection::Base
-
end
-
raise NonInferrableConnectionError.new(name) if connection.nil?
-
connection
-
end
-
end
-
-
# Performs connection attempt to exert #connect on the connection under test.
-
#
-
# Accepts request path as the first argument and the following request options:
-
#
-
# - params – URL parameters (Hash)
-
# - headers – request headers (Hash)
-
# - session – session data (Hash)
-
# - env – additional Rack env configuration (Hash)
-
1
def connect(path = ActionCable.server.config.mount_path, **request_params)
-
path ||= DEFAULT_PATH
-
-
connection = self.class.connection_class.allocate
-
connection.singleton_class.include(TestConnection)
-
connection.send(:initialize, build_test_request(path, **request_params))
-
connection.connect if connection.respond_to?(:connect)
-
-
# Only set instance variable if connected successfully
-
@connection = connection
-
end
-
-
# Exert #disconnect on the connection under test.
-
1
def disconnect
-
raise "Must be connected!" if connection.nil?
-
-
connection.disconnect if connection.respond_to?(:disconnect)
-
@connection = nil
-
end
-
-
1
def cookies
-
@cookie_jar ||= TestCookieJar.new
-
end
-
-
1
private
-
1
def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
-
wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
-
-
uri = URI.parse(path)
-
-
query_string = params.nil? ? uri.query : params.to_query
-
-
request_env = {
-
"QUERY_STRING" => query_string,
-
"PATH_INFO" => uri.path
-
}.merge(env)
-
-
if wrapped_headers.present?
-
ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
-
end
-
-
TestRequest.create(request_env).tap do |request|
-
request.session = session.with_indifferent_access
-
request.cookie_jar = cookies
-
end
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
# Provides helper methods for testing Action Cable broadcasting
-
1
module TestHelper
-
1
def before_setup # :nodoc:
-
server = ActionCable.server
-
test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
-
-
@old_pubsub_adapter = server.pubsub
-
-
server.instance_variable_set(:@pubsub, test_adapter)
-
super
-
end
-
-
1
def after_teardown # :nodoc:
-
super
-
ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
-
end
-
-
# Asserts that the number of broadcasted messages to the stream matches the given number.
-
#
-
# def test_broadcasts
-
# assert_broadcasts 'messages', 0
-
# ActionCable.server.broadcast 'messages', { text: 'hello' }
-
# assert_broadcasts 'messages', 1
-
# ActionCable.server.broadcast 'messages', { text: 'world' }
-
# assert_broadcasts 'messages', 2
-
# end
-
#
-
# If a block is passed, that block should cause the specified number of
-
# messages to be broadcasted.
-
#
-
# def test_broadcasts_again
-
# assert_broadcasts('messages', 1) do
-
# ActionCable.server.broadcast 'messages', { text: 'hello' }
-
# end
-
#
-
# assert_broadcasts('messages', 2) do
-
# ActionCable.server.broadcast 'messages', { text: 'hi' }
-
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
-
# end
-
# end
-
#
-
1
def assert_broadcasts(stream, number, &block)
-
if block_given?
-
original_count = broadcasts_size(stream)
-
assert_nothing_raised(&block)
-
new_count = broadcasts_size(stream)
-
actual_count = new_count - original_count
-
else
-
actual_count = broadcasts_size(stream)
-
end
-
-
assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
-
end
-
-
# Asserts that no messages have been sent to the stream.
-
#
-
# def test_no_broadcasts
-
# assert_no_broadcasts 'messages'
-
# ActionCable.server.broadcast 'messages', { text: 'hi' }
-
# assert_broadcasts 'messages', 1
-
# end
-
#
-
# If a block is passed, that block should not cause any message to be sent.
-
#
-
# def test_broadcasts_again
-
# assert_no_broadcasts 'messages' do
-
# # No job messages should be sent from this block
-
# end
-
# end
-
#
-
# Note: This assertion is simply a shortcut for:
-
#
-
# assert_broadcasts 'messages', 0, &block
-
#
-
1
def assert_no_broadcasts(stream, &block)
-
assert_broadcasts stream, 0, &block
-
end
-
-
# Asserts that the specified message has been sent to the stream.
-
#
-
# def test_assert_transmitted_message
-
# ActionCable.server.broadcast 'messages', text: 'hello'
-
# assert_broadcast_on('messages', text: 'hello')
-
# end
-
#
-
# If a block is passed, that block should cause a message with the specified data to be sent.
-
#
-
# def test_assert_broadcast_on_again
-
# assert_broadcast_on('messages', text: 'hello') do
-
# ActionCable.server.broadcast 'messages', text: 'hello'
-
# end
-
# end
-
#
-
1
def assert_broadcast_on(stream, data, &block)
-
# Encode to JSON and back–we want to use this value to compare
-
# with decoded JSON.
-
# Comparing JSON strings doesn't work due to the order if the keys.
-
serialized_msg =
-
ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
-
-
new_messages = broadcasts(stream)
-
if block_given?
-
old_messages = new_messages
-
clear_messages(stream)
-
-
assert_nothing_raised(&block)
-
new_messages = broadcasts(stream)
-
clear_messages(stream)
-
-
# Restore all sent messages
-
(old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
-
end
-
-
message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
-
-
assert message, "No messages sent with #{data} to #{stream}"
-
end
-
-
1
def pubsub_adapter # :nodoc:
-
ActionCable.server.pubsub
-
end
-
-
1
delegate :broadcasts, :clear_messages, to: :pubsub_adapter
-
-
1
private
-
1
def broadcasts_size(channel)
-
broadcasts(channel).size
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "mail"
-
-
1
module ActionMailbox
-
1
module TestHelper
-
# Create an +InboundEmail+ record using an eml fixture in the format of message/rfc822
-
# referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
-
1
def create_inbound_email_from_fixture(fixture_name, status: :processing)
-
create_inbound_email_from_source file_fixture(fixture_name).read, status: status
-
end
-
-
# Creates an +InboundEmail+ by specifying through options or a block.
-
#
-
# ==== Options
-
#
-
# * <tt>:status</tt> - The +status+ to set for the created +InboundEmail+.
-
# For possible statuses, see {its documentation}[rdoc-ref:ActionMailbox::InboundEmail].
-
#
-
# ==== Creating a simple email
-
#
-
# When you only need to set basic fields like +from+, +to+, +subject+, and
-
# +body+, you can pass them directly as options.
-
#
-
# create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!")
-
#
-
# ==== Creating a multi-part email
-
#
-
# When you need to create a more intricate email, like a multi-part email
-
# that contains both a plaintext version and an HTML version, you can pass a
-
# block.
-
#
-
# create_inbound_email_from_mail do
-
# to "David Heinemeier Hansson <david@loudthinking.com>"
-
# from "Bilbo Baggins <bilbo@bagend.com>"
-
# subject "Come down to the Shire!"
-
#
-
# text_part do
-
# body "Please join us for a party at Bag End"
-
# end
-
#
-
# html_part do
-
# body "<h1>Please join us for a party at Bag End</h1>"
-
# end
-
# end
-
#
-
# As with +Mail.new+, you can also use a block parameter to define the parts
-
# of the message:
-
#
-
# create_inbound_email_from_mail do |mail|
-
# mail.to "David Heinemeier Hansson <david@loudthinking.com>"
-
# mail.from "Bilbo Baggins <bilbo@bagend.com>"
-
# mail.subject "Come down to the Shire!"
-
#
-
# mail.text_part do |part|
-
# part.body "Please join us for a party at Bag End"
-
# end
-
#
-
# mail.html_part do |part|
-
# part.body "<h1>Please join us for a party at Bag End</h1>"
-
# end
-
# end
-
1
def create_inbound_email_from_mail(status: :processing, **mail_options, &block)
-
mail = Mail.new(mail_options, &block)
-
# Bcc header is not encoded by default
-
mail[:bcc].include_in_headers = true if mail[:bcc]
-
-
create_inbound_email_from_source mail.to_s, status: status
-
end
-
-
# Create an +InboundEmail+ using the raw rfc822 +source+ as text.
-
1
def create_inbound_email_from_source(source, status: :processing)
-
ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
-
end
-
-
-
# Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_fixture+
-
# and immediately route it to processing.
-
1
def receive_inbound_email_from_fixture(*args)
-
create_inbound_email_from_fixture(*args).tap(&:route)
-
end
-
-
# Create an +InboundEmail+ using the same options or block as
-
# {create_inbound_email_from_mail}[rdoc-ref:#create_inbound_email_from_mail],
-
# then immediately route it for processing.
-
1
def receive_inbound_email_from_mail(**kwargs, &block)
-
create_inbound_email_from_mail(**kwargs, &block).tap(&:route)
-
end
-
-
# Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_source+ and immediately route it
-
# to processing.
-
1
def receive_inbound_email_from_source(*args)
-
create_inbound_email_from_source(*args).tap(&:route)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "mail"
-
1
require "action_mailer/collector"
-
1
require "active_support/core_ext/string/inflections"
-
1
require "active_support/core_ext/hash/except"
-
1
require "active_support/core_ext/module/anonymous"
-
-
1
require "action_mailer/log_subscriber"
-
1
require "action_mailer/rescuable"
-
-
1
module ActionMailer
-
# Action Mailer allows you to send email from your application using a mailer model and views.
-
#
-
# = Mailer Models
-
#
-
# To use Action Mailer, you need to create a mailer model.
-
#
-
# $ bin/rails generate mailer Notifier
-
#
-
# The generated model inherits from <tt>ApplicationMailer</tt> which in turn
-
# inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods
-
# used to generate an email message. In these methods, you can set up variables to be used in
-
# the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments.
-
#
-
# class ApplicationMailer < ActionMailer::Base
-
# default from: 'from@example.com'
-
# layout 'mailer'
-
# end
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default from: 'no-reply@example.com',
-
# return_path: 'system@example.com'
-
#
-
# def welcome(recipient)
-
# @account = recipient
-
# mail(to: recipient.email_address_with_name,
-
# bcc: ["bcc@example.com", "Order Watcher <watcher@example.com>"])
-
# end
-
# end
-
#
-
# Within the mailer method, you have access to the following methods:
-
#
-
# * <tt>attachments[]=</tt> - Allows you to add attachments to your email in an intuitive
-
# manner; <tt>attachments['filename.png'] = File.read('path/to/filename.png')</tt>
-
#
-
# * <tt>attachments.inline[]=</tt> - Allows you to add an inline attachment to your email
-
# in the same manner as <tt>attachments[]=</tt>
-
#
-
# * <tt>headers[]=</tt> - Allows you to specify any header field in your email such
-
# as <tt>headers['X-No-Spam'] = 'True'</tt>. Note that declaring a header multiple times
-
# will add many fields of the same name. Read #headers doc for more information.
-
#
-
# * <tt>headers(hash)</tt> - Allows you to specify multiple headers in your email such
-
# as <tt>headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})</tt>
-
#
-
# * <tt>mail</tt> - Allows you to specify email to be sent.
-
#
-
# The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt>
-
# will accept (any valid email header including optional fields).
-
#
-
# The +mail+ method, if not passed a block, will inspect your views and send all the views with
-
# the same name as the method, so the above action would send the +welcome.text.erb+ view
-
# file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email.
-
#
-
# If you want to explicitly render only certain templates, pass a block:
-
#
-
# mail(to: user.email) do |format|
-
# format.text
-
# format.html
-
# end
-
#
-
# The block syntax is also useful in providing information specific to a part:
-
#
-
# mail(to: user.email) do |format|
-
# format.text(content_transfer_encoding: "base64")
-
# format.html
-
# end
-
#
-
# Or even to render a special view:
-
#
-
# mail(to: user.email) do |format|
-
# format.text
-
# format.html { render "some_other_template" }
-
# end
-
#
-
# = Mailer views
-
#
-
# Like Action Controller, each mailer class has a corresponding view directory in which each
-
# method of the class looks for a template with its name.
-
#
-
# To define a template to be used with a mailer, create an <tt>.erb</tt> file with the same
-
# name as the method in your mailer model. For example, in the mailer defined above, the template at
-
# <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email.
-
#
-
# Variables defined in the methods of your mailer model are accessible as instance variables in their
-
# corresponding view.
-
#
-
# Emails by default are sent in plain text, so a sample view for our model example might look like this:
-
#
-
# Hi <%= @account.name %>,
-
# Thanks for joining our service! Please check back often.
-
#
-
# You can even use Action View helpers in these views. For example:
-
#
-
# You got a new note!
-
# <%= truncate(@note.body, length: 25) %>
-
#
-
# If you need to access the subject, from or the recipients in the view, you can do that through message object:
-
#
-
# You got a new note from <%= message.from %>!
-
# <%= truncate(@note.body, length: 25) %>
-
#
-
#
-
# = Generating URLs
-
#
-
# URLs can be generated in mailer views using <tt>url_for</tt> or named routes. Unlike controllers from
-
# Action Pack, the mailer instance doesn't have any context about the incoming request, so you'll need
-
# to provide all of the details needed to generate a URL.
-
#
-
# When using <tt>url_for</tt> you'll need to provide the <tt>:host</tt>, <tt>:controller</tt>, and <tt>:action</tt>:
-
#
-
# <%= url_for(host: "example.com", controller: "welcome", action: "greeting") %>
-
#
-
# When using named routes you only need to supply the <tt>:host</tt>:
-
#
-
# <%= users_url(host: "example.com") %>
-
#
-
# You should use the <tt>named_route_url</tt> style (which generates absolute URLs) and avoid using the
-
# <tt>named_route_path</tt> style (which generates relative URLs), since clients reading the mail will
-
# have no concept of a current URL from which to determine a relative path.
-
#
-
# It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt>
-
# option as a configuration option in <tt>config/application.rb</tt>:
-
#
-
# config.action_mailer.default_url_options = { host: "example.com" }
-
#
-
# You can also define a <tt>default_url_options</tt> method on individual mailers to override these
-
# default settings per-mailer.
-
#
-
# By default when <tt>config.force_ssl</tt> is +true+, URLs generated for hosts will use the HTTPS protocol.
-
#
-
# = Sending mail
-
#
-
# Once a mailer action and template are defined, you can deliver your message or defer its creation and
-
# delivery for later:
-
#
-
# NotifierMailer.welcome(User.first).deliver_now # sends the email
-
# mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object
-
# mail.deliver_now # generates and sends the email now
-
#
-
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call
-
# your method to generate the mail. If you want direct access to the delegator, or <tt>Mail::Message</tt>,
-
# you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object.
-
#
-
# NotifierMailer.welcome(User.first).message # => a Mail::Message object
-
#
-
# Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background
-
# (example: outside of the request-response cycle, so the user doesn't have to wait on it):
-
#
-
# NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job
-
#
-
# Note that <tt>deliver_later</tt> will execute your method from the background job.
-
#
-
# You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
-
# All instance methods are expected to return a message object to be sent.
-
#
-
# = Multipart Emails
-
#
-
# Multipart messages can also be used implicitly because Action Mailer will automatically detect and use
-
# multipart templates, where each template is named after the name of the action, followed by the content
-
# type. Each such detected template will be added to the message, as a separate part.
-
#
-
# For example, if the following templates exist:
-
# * signup_notification.text.erb
-
# * signup_notification.html.erb
-
# * signup_notification.xml.builder
-
# * signup_notification.yml.erb
-
#
-
# Each would be rendered and added as a separate part to the message, with the corresponding content
-
# type. The content type for the entire message is automatically set to <tt>multipart/alternative</tt>,
-
# which indicates that the email contains multiple different representations of the same email
-
# body. The same instance variables defined in the action are passed to all email templates.
-
#
-
# Implicit template rendering is not performed if any attachments or parts have been added to the email.
-
# This means that you'll have to manually add each part to the email and set the content type of the email
-
# to <tt>multipart/alternative</tt>.
-
#
-
# = Attachments
-
#
-
# Sending attachment in emails is easy:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
-
# mail(to: recipient, subject: "New account information")
-
# end
-
# end
-
#
-
# Which will (if it had both a <tt>welcome.text.erb</tt> and <tt>welcome.html.erb</tt>
-
# template in the view directory), send a complete <tt>multipart/mixed</tt> email with two parts,
-
# the first part being a <tt>multipart/alternative</tt> with the text and HTML email parts inside,
-
# and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book
-
# with the filename +free_book.pdf+.
-
#
-
# If you need to send attachments with no content, you need to create an empty view for it,
-
# or add an empty body parameter like this:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
-
# mail(to: recipient, subject: "New account information", body: "")
-
# end
-
# end
-
#
-
# You can also send attachments with html template, in this case you need to add body, attachments,
-
# and custom content type like this:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments["free_book.pdf"] = File.read("path/to/file.pdf")
-
# mail(to: recipient,
-
# subject: "New account information",
-
# content_type: "text/html",
-
# body: "<html><body>Hello there</body></html>")
-
# end
-
# end
-
#
-
# = Inline Attachments
-
#
-
# You can also specify that a file should be displayed inline with other HTML. This is useful
-
# if you want to display a corporate logo or a photo.
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments.inline['photo.png'] = File.read('path/to/photo.png')
-
# mail(to: recipient, subject: "Here is what we look like")
-
# end
-
# end
-
#
-
# And then to reference the image in the view, you create a <tt>welcome.html.erb</tt> file and
-
# make a call to +image_tag+ passing in the attachment you want to display and then call
-
# +url+ on the attachment to get the relative content id path for the image source:
-
#
-
# <h1>Please Don't Cringe</h1>
-
#
-
# <%= image_tag attachments['photo.png'].url -%>
-
#
-
# As we are using Action View's +image_tag+ method, you can pass in any other options you want:
-
#
-
# <h1>Please Don't Cringe</h1>
-
#
-
# <%= image_tag attachments['photo.png'].url, alt: 'Our Photo', class: 'photo' -%>
-
#
-
# = Observing and Intercepting Mails
-
#
-
# Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to
-
# register classes that are called during the mail delivery life cycle.
-
#
-
# An observer class must implement the <tt>:delivered_email(message)</tt> method which will be
-
# called once for every email sent after the email has been sent.
-
#
-
# An interceptor class must implement the <tt>:delivering_email(message)</tt> method which will be
-
# called before the email is sent, allowing you to make modifications to the email before it hits
-
# the delivery agents. Your class should make any needed modifications directly to the passed
-
# in <tt>Mail::Message</tt> instance.
-
#
-
# = Default Hash
-
#
-
# Action Mailer provides some intelligent defaults for your emails, these are usually specified in a
-
# default method inside the class definition:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default sender: 'system@example.com'
-
# end
-
#
-
# You can pass in any header value that a <tt>Mail::Message</tt> accepts. Out of the box,
-
# <tt>ActionMailer::Base</tt> sets the following:
-
#
-
# * <tt>mime_version: "1.0"</tt>
-
# * <tt>charset: "UTF-8"</tt>
-
# * <tt>content_type: "text/plain"</tt>
-
# * <tt>parts_order: [ "text/plain", "text/enriched", "text/html" ]</tt>
-
#
-
# <tt>parts_order</tt> and <tt>charset</tt> are not actually valid <tt>Mail::Message</tt> header fields,
-
# but Action Mailer translates them appropriately and sets the correct values.
-
#
-
# As you can pass in any header, you need to either quote the header as a string, or pass it in as
-
# an underscored symbol, so the following will work:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default 'Content-Transfer-Encoding' => '7bit',
-
# content_description: 'This is a description'
-
# end
-
#
-
# Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash,
-
# so you can define methods that evaluate as the message is being generated:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address }
-
#
-
# private
-
# def my_method
-
# 'some complex call'
-
# end
-
# end
-
#
-
# Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you
-
# set something in the default hash using a proc, and then set the same thing inside of your
-
# mailer method, it will get overwritten by the mailer method.
-
#
-
# It is also possible to set these default options that will be used in all mailers through
-
# the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>:
-
#
-
# config.action_mailer.default_options = { from: "no-reply@example.org" }
-
#
-
# = Callbacks
-
#
-
# You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages.
-
# This may be useful, for example, when you want to add default inline attachments for all
-
# messages sent out by a certain mailer class:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# before_action :add_inline_attachment!
-
#
-
# def welcome
-
# mail
-
# end
-
#
-
# private
-
# def add_inline_attachment!
-
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
-
# end
-
# end
-
#
-
# Callbacks in Action Mailer are implemented using
-
# <tt>AbstractController::Callbacks</tt>, so you can define and configure
-
# callbacks in the same manner that you would use callbacks in classes that
-
# inherit from <tt>ActionController::Base</tt>.
-
#
-
# Note that unless you have a specific reason to do so, you should prefer
-
# using <tt>before_action</tt> rather than <tt>after_action</tt> in your
-
# Action Mailer classes so that headers are parsed properly.
-
#
-
# = Previewing emails
-
#
-
# You can preview your email templates visually by adding a mailer preview file to the
-
# <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
-
# with database data, you'll need to write some scenarios to load messages with fake data:
-
#
-
# class NotifierMailerPreview < ActionMailer::Preview
-
# def welcome
-
# NotifierMailer.welcome(User.first)
-
# end
-
# end
-
#
-
# Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer
-
# method without the additional <tt>deliver_now</tt> / <tt>deliver_later</tt>. The location of the
-
# mailer previews directory can be configured using the <tt>preview_path</tt> option which has a default
-
# of <tt>test/mailers/previews</tt>:
-
#
-
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
-
#
-
# An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt>
-
# on a running development server instance.
-
#
-
# Previews can also be intercepted in a similar manner as deliveries can be by registering
-
# a preview interceptor that has a <tt>previewing_email</tt> method:
-
#
-
# class CssInlineStyler
-
# def self.previewing_email(message)
-
# # inline CSS styles
-
# end
-
# end
-
#
-
# config.action_mailer.preview_interceptors :css_inline_styler
-
#
-
# Note that interceptors need to be registered both with <tt>register_interceptor</tt>
-
# and <tt>register_preview_interceptor</tt> if they should operate on both sending and
-
# previewing emails.
-
#
-
# = Configuration options
-
#
-
# These options are specified on the class level, like
-
# <tt>ActionMailer::Base.raise_delivery_errors = true</tt>
-
#
-
# * <tt>default_options</tt> - You can pass this in at a class level as well as within the class itself as
-
# per the above section.
-
#
-
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
-
# Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
-
#
-
# * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
-
# * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default
-
# "localhost" setting.
-
# * <tt>:port</tt> - On the off chance that your mail server doesn't run on port 25, you can change it.
-
# * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.
-
# * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.
-
# * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
-
# * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the
-
# authentication type here.
-
# This is a symbol and one of <tt>:plain</tt> (will send the password Base64 encoded), <tt>:login</tt> (will
-
# send the password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange
-
# information and a cryptographic Message Digest 5 algorithm to hash important information)
-
# * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts
-
# to use it. Defaults to <tt>true</tt>.
-
# * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
-
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
-
# of an OpenSSL verify constant (<tt>'none'</tt> or <tt>'peer'</tt>) or directly the constant
-
# (<tt>OpenSSL::SSL::VERIFY_NONE</tt> or <tt>OpenSSL::SSL::VERIFY_PEER</tt>).
-
# * <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)
-
#
-
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
-
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
-
# * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i</tt> with <tt>-f sender@address</tt>
-
# added automatically before the message is sent.
-
#
-
# * <tt>file_settings</tt> - Allows you to override options for the <tt>:file</tt> delivery method.
-
# * <tt>:location</tt> - The directory into which emails will be written. Defaults to the application
-
# <tt>tmp/mails</tt>.
-
#
-
# * <tt>raise_delivery_errors</tt> - Whether or not errors should be raised if the email fails to be delivered.
-
#
-
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default),
-
# <tt>:sendmail</tt>, <tt>:test</tt>, and <tt>:file</tt>. Or you may provide a custom delivery method
-
# object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to
-
# implement for a custom delivery agent.
-
#
-
# * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you
-
# call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can
-
# be turned off to aid in functional testing.
-
#
-
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
-
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
-
#
-
# * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+.
-
1
class Base < AbstractController::Base
-
1
include DeliveryMethods
-
1
include Rescuable
-
1
include Parameterized
-
1
include Previews
-
-
1
abstract!
-
-
1
include AbstractController::Rendering
-
-
1
include AbstractController::Logger
-
1
include AbstractController::Helpers
-
1
include AbstractController::Translation
-
1
include AbstractController::AssetPaths
-
1
include AbstractController::Callbacks
-
1
include AbstractController::Caching
-
-
1
include ActionView::Layouts
-
-
1
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
-
-
1
helper ActionMailer::MailHelper
-
-
1
class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob
-
1
class_attribute :default_params, default: {
-
mime_version: "1.0",
-
charset: "UTF-8",
-
content_type: "text/plain",
-
parts_order: [ "text/plain", "text/enriched", "text/html" ]
-
}.freeze
-
-
1
class << self
-
# Register one or more Observers which will be notified when mail is delivered.
-
1
def register_observers(*observers)
-
1
observers.flatten.compact.each { |observer| register_observer(observer) }
-
end
-
-
# Unregister one or more previously registered Observers.
-
1
def unregister_observers(*observers)
-
observers.flatten.compact.each { |observer| unregister_observer(observer) }
-
end
-
-
# Register one or more Interceptors which will be called before mail is sent.
-
1
def register_interceptors(*interceptors)
-
1
interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
-
end
-
-
# Unregister one or more previously registered Interceptors.
-
1
def unregister_interceptors(*interceptors)
-
interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) }
-
end
-
-
# Register an Observer which will be notified when mail is delivered.
-
# Either a class, string or symbol can be passed in as the Observer.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
1
def register_observer(observer)
-
Mail.register_observer(observer_class_for(observer))
-
end
-
-
# Unregister a previously registered Observer.
-
# Either a class, string or symbol can be passed in as the Observer.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
1
def unregister_observer(observer)
-
Mail.unregister_observer(observer_class_for(observer))
-
end
-
-
# Register an Interceptor which will be called before mail is sent.
-
# Either a class, string or symbol can be passed in as the Interceptor.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
1
def register_interceptor(interceptor)
-
Mail.register_interceptor(observer_class_for(interceptor))
-
end
-
-
# Unregister a previously registered Interceptor.
-
# Either a class, string or symbol can be passed in as the Interceptor.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
1
def unregister_interceptor(interceptor)
-
Mail.unregister_interceptor(observer_class_for(interceptor))
-
end
-
-
1
def observer_class_for(value) # :nodoc:
-
case value
-
when String, Symbol
-
value.to_s.camelize.constantize
-
else
-
value
-
end
-
end
-
1
private :observer_class_for
-
-
# Returns the name of the current mailer. This method is also being used as a path for a view lookup.
-
# If this is an anonymous mailer, this method will return +anonymous+ instead.
-
1
def mailer_name
-
@mailer_name ||= anonymous? ? "anonymous" : name.underscore
-
end
-
# Allows to set the name of current mailer.
-
1
attr_writer :mailer_name
-
1
alias :controller_path :mailer_name
-
-
# Sets the defaults through app configuration:
-
#
-
# config.action_mailer.default(from: "no-reply@example.org")
-
#
-
# Aliased by ::default_options=
-
1
def default(value = nil)
-
self.default_params = default_params.merge(value).freeze if value
-
default_params
-
end
-
# Allows to set defaults through app configuration:
-
#
-
# config.action_mailer.default_options = { from: "no-reply@example.org" }
-
1
alias :default_options= :default
-
-
# Wraps an email delivery inside of <tt>ActiveSupport::Notifications</tt> instrumentation.
-
#
-
# This method is actually called by the <tt>Mail::Message</tt> object itself
-
# through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
-
# calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
-
# nothing except tell the logger you sent the email.
-
1
def deliver_mail(mail) #:nodoc:
-
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
-
set_payload_for_mail(payload, mail)
-
yield # Let Mail do the delivery actions
-
end
-
end
-
-
# Returns an email in the format "Name <email@example.com>".
-
1
def email_address_with_name(address, name)
-
Mail::Address.new.tap do |builder|
-
builder.address = address
-
builder.display_name = name
-
end.to_s
-
end
-
-
1
private
-
1
def set_payload_for_mail(payload, mail)
-
payload[:mail] = mail.encoded
-
payload[:mailer] = name
-
payload[:message_id] = mail.message_id
-
payload[:subject] = mail.subject
-
payload[:to] = mail.to
-
payload[:from] = mail.from
-
payload[:bcc] = mail.bcc if mail.bcc.present?
-
payload[:cc] = mail.cc if mail.cc.present?
-
payload[:date] = mail.date
-
payload[:perform_deliveries] = mail.perform_deliveries
-
end
-
-
1
def method_missing(method_name, *args)
-
if action_methods.include?(method_name.to_s)
-
MessageDelivery.new(self, method_name, *args)
-
else
-
super
-
end
-
end
-
1
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
-
-
1
def respond_to_missing?(method, include_all = false)
-
1
action_methods.include?(method.to_s) || super
-
end
-
end
-
-
1
attr_internal :message
-
-
1
def initialize
-
super()
-
@_mail_was_called = false
-
@_message = Mail.new
-
end
-
-
1
def process(method_name, *args) #:nodoc:
-
payload = {
-
mailer: self.class.name,
-
action: method_name,
-
args: args
-
}
-
-
ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
-
super
-
@_message = NullMail.new unless @_mail_was_called
-
end
-
end
-
-
1
class NullMail #:nodoc:
-
1
def body; "" end
-
1
def header; {} end
-
-
1
def respond_to?(string, include_all = false)
-
true
-
end
-
-
1
def method_missing(*args)
-
nil
-
end
-
end
-
-
# Returns the name of the mailer object.
-
1
def mailer_name
-
self.class.mailer_name
-
end
-
-
# Returns an email in the format "Name <email@example.com>".
-
1
def email_address_with_name(address, name)
-
self.class.email_address_with_name(address, name)
-
end
-
-
# Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt>
-
# object which will add them to itself.
-
#
-
# headers['X-Special-Domain-Specific-Header'] = "SecretValue"
-
#
-
# You can also pass a hash into headers of header field names and values,
-
# which will then be set on the <tt>Mail::Message</tt> object:
-
#
-
# headers 'X-Special-Domain-Specific-Header' => "SecretValue",
-
# 'In-Reply-To' => incoming.message_id
-
#
-
# The resulting <tt>Mail::Message</tt> will have the following in its header:
-
#
-
# X-Special-Domain-Specific-Header: SecretValue
-
#
-
# Note about replacing already defined headers:
-
#
-
# * +subject+
-
# * +sender+
-
# * +from+
-
# * +to+
-
# * +cc+
-
# * +bcc+
-
# * +reply-to+
-
# * +orig-date+
-
# * +message-id+
-
# * +references+
-
#
-
# Fields can only appear once in email headers while other fields such as
-
# <tt>X-Anything</tt> can appear multiple times.
-
#
-
# If you want to replace any header which already exists, first set it to
-
# +nil+ in order to reset the value otherwise another field will be added
-
# for the same header.
-
1
def headers(args = nil)
-
if args
-
@_message.headers(args)
-
else
-
@_message
-
end
-
end
-
-
# Allows you to add attachments to an email, like so:
-
#
-
# mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
-
#
-
# If you do this, then Mail will take the file name and work out the mime type.
-
# It will also set the Content-Type, Content-Disposition, Content-Transfer-Encoding
-
# and encode the contents of the attachment in Base64.
-
#
-
# You can also specify overrides if you want by passing a hash instead of a string:
-
#
-
# mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
-
# content: File.read('/path/to/filename.jpg')}
-
#
-
# If you want to use encoding other than Base64 then you will need to pass encoding
-
# type along with the pre-encoded content as Mail doesn't know how to decode the
-
# data:
-
#
-
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
-
# mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
-
# encoding: 'SpecialEncoding',
-
# content: file_content }
-
#
-
# You can also search for specific attachments:
-
#
-
# # By Filename
-
# mail.attachments['filename.jpg'] # => Mail::Part object or nil
-
#
-
# # or by index
-
# mail.attachments[0] # => Mail::Part (first attachment)
-
#
-
1
def attachments
-
if @_mail_was_called
-
LateAttachmentsProxy.new(@_message.attachments)
-
else
-
@_message.attachments
-
end
-
end
-
-
1
class LateAttachmentsProxy < SimpleDelegator
-
1
def inline; self end
-
1
def []=(_name, _content); _raise_error end
-
-
1
private
-
1
def _raise_error
-
raise RuntimeError, "Can't add attachments after `mail` was called.\n" \
-
"Make sure to use `attachments[]=` before calling `mail`."
-
end
-
end
-
-
# The main method that creates the message and renders the email templates. There are
-
# two ways to call this method, with a block, or without a block.
-
#
-
# It accepts a headers hash. This hash allows you to specify
-
# the most used headers in an email message, these are:
-
#
-
# * +:subject+ - The subject of the message, if this is omitted, Action Mailer will
-
# ask the Rails I18n class for a translated +:subject+ in the scope of
-
# <tt>[mailer_scope, action_name]</tt> or if this is missing, will translate the
-
# humanized version of the +action_name+
-
# * +:to+ - Who the message is destined for, can be a string of addresses, or an array
-
# of addresses.
-
# * +:from+ - Who the message is from
-
# * +:cc+ - Who you would like to Carbon-Copy on this email, can be a string of addresses,
-
# or an array of addresses.
-
# * +:bcc+ - Who you would like to Blind-Carbon-Copy on this email, can be a string of
-
# addresses, or an array of addresses.
-
# * +:reply_to+ - Who to set the Reply-To header of the email to.
-
# * +:date+ - The date to say the email was sent on.
-
#
-
# You can set default values for any of the above headers (except +:date+)
-
# by using the ::default class method:
-
#
-
# class Notifier < ActionMailer::Base
-
# default from: 'no-reply@test.lindsaar.net',
-
# bcc: 'email_logger@test.lindsaar.net',
-
# reply_to: 'bounces@test.lindsaar.net'
-
# end
-
#
-
# If you need other headers not listed above, you can either pass them in
-
# as part of the headers hash or use the <tt>headers['name'] = value</tt>
-
# method.
-
#
-
# When a +:return_path+ is specified as header, that value will be used as
-
# the 'envelope from' address for the Mail message. Setting this is useful
-
# when you want delivery notifications sent to a different address than the
-
# one in +:from+. Mail will actually use the +:return_path+ in preference
-
# to the +:sender+ in preference to the +:from+ field for the 'envelope
-
# from' value.
-
#
-
# If you do not pass a block to the +mail+ method, it will find all
-
# templates in the view paths using by default the mailer name and the
-
# method name that it is being called from, it will then create parts for
-
# each of these templates intelligently, making educated guesses on correct
-
# content type and sequence, and return a fully prepared <tt>Mail::Message</tt>
-
# ready to call <tt>:deliver</tt> on to send.
-
#
-
# For example:
-
#
-
# class Notifier < ActionMailer::Base
-
# default from: 'no-reply@test.lindsaar.net'
-
#
-
# def welcome
-
# mail(to: 'mikel@test.lindsaar.net')
-
# end
-
# end
-
#
-
# Will look for all templates at "app/views/notifier" with name "welcome".
-
# If no welcome template exists, it will raise an ActionView::MissingTemplate error.
-
#
-
# However, those can be customized:
-
#
-
# mail(template_path: 'notifications', template_name: 'another')
-
#
-
# And now it will look for all templates at "app/views/notifications" with name "another".
-
#
-
# If you do pass a block, you can render specific templates of your choice:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text
-
# format.html
-
# end
-
#
-
# You can even render plain text directly without using a template:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text { render plain: "Hello Mikel!" }
-
# format.html { render html: "<h1>Hello Mikel!</h1>".html_safe }
-
# end
-
#
-
# Which will render a +multipart/alternative+ email with +text/plain+ and
-
# +text/html+ parts.
-
#
-
# The block syntax also allows you to customize the part headers if desired:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text(content_transfer_encoding: "base64")
-
# format.html
-
# end
-
#
-
1
def mail(headers = {}, &block)
-
return message if @_mail_was_called && headers.blank? && !block
-
-
# At the beginning, do not consider class default for content_type
-
content_type = headers[:content_type]
-
-
headers = apply_defaults(headers)
-
-
# Apply charset at the beginning so all fields are properly quoted
-
message.charset = charset = headers[:charset]
-
-
# Set configure delivery behavior
-
wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options])
-
-
assign_headers_to_message(message, headers)
-
-
# Render the templates and blocks
-
responses = collect_responses(headers, &block)
-
@_mail_was_called = true
-
-
create_parts_from_responses(message, responses)
-
wrap_inline_attachments(message)
-
-
# Set up content type, reapply charset and handle parts order
-
message.content_type = set_content_type(message, content_type, headers[:content_type])
-
message.charset = charset
-
-
if message.multipart?
-
message.body.set_sort_order(headers[:parts_order])
-
message.body.sort_parts!
-
end
-
-
message
-
end
-
-
1
private
-
# Used by #mail to set the content type of the message.
-
#
-
# It will use the given +user_content_type+, or multipart if the mail
-
# message has any attachments. If the attachments are inline, the content
-
# type will be "multipart/related", otherwise "multipart/mixed".
-
#
-
# If there is no content type passed in via headers, and there are no
-
# attachments, or the message is multipart, then the default content type is
-
# used.
-
1
def set_content_type(m, user_content_type, class_default) # :doc:
-
params = m.content_type_parameters || {}
-
case
-
when user_content_type.present?
-
user_content_type
-
when m.has_attachments?
-
if m.attachments.all?(&:inline?)
-
["multipart", "related", params]
-
else
-
["multipart", "mixed", params]
-
end
-
when m.multipart?
-
["multipart", "alternative", params]
-
else
-
m.content_type || class_default
-
end
-
end
-
-
# Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope.
-
# If it does not find a translation for the +subject+ under the specified scope it will default to a
-
# humanized version of the <tt>action_name</tt>.
-
# If the subject has interpolations, you can pass them through the +interpolations+ parameter.
-
1
def default_i18n_subject(interpolations = {}) # :doc:
-
mailer_scope = self.class.mailer_name.tr("/", ".")
-
I18n.t(:subject, **interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
-
end
-
-
# Emails do not support relative path links.
-
1
def self.supports_path? # :doc:
-
false
-
end
-
-
1
def apply_defaults(headers)
-
default_values = self.class.default.transform_values do |value|
-
compute_default(value)
-
end
-
-
headers_with_defaults = headers.reverse_merge(default_values)
-
headers_with_defaults[:subject] ||= default_i18n_subject
-
headers_with_defaults
-
end
-
-
1
def compute_default(value)
-
return value unless value.is_a?(Proc)
-
-
if value.arity == 1
-
instance_exec(self, &value)
-
else
-
instance_exec(&value)
-
end
-
end
-
-
1
def assign_headers_to_message(message, headers)
-
assignable = headers.except(:parts_order, :content_type, :body, :template_name,
-
:template_path, :delivery_method, :delivery_method_options)
-
assignable.each { |k, v| message[k] = v }
-
end
-
-
1
def collect_responses(headers, &block)
-
if block_given?
-
collect_responses_from_block(headers, &block)
-
elsif headers[:body]
-
collect_responses_from_text(headers)
-
else
-
collect_responses_from_templates(headers)
-
end
-
end
-
-
1
def collect_responses_from_block(headers)
-
templates_name = headers[:template_name] || action_name
-
collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) }
-
yield(collector)
-
collector.responses
-
end
-
-
1
def collect_responses_from_text(headers)
-
[{
-
body: headers.delete(:body),
-
content_type: headers[:content_type] || "text/plain"
-
}]
-
end
-
-
1
def collect_responses_from_templates(headers)
-
templates_path = headers[:template_path] || self.class.mailer_name
-
templates_name = headers[:template_name] || action_name
-
-
each_template(Array(templates_path), templates_name).map do |template|
-
format = template.format || self.formats.first
-
{
-
body: render(template: template, formats: [format]),
-
content_type: Mime[format].to_s
-
}
-
end
-
end
-
-
1
def each_template(paths, name, &block)
-
templates = lookup_context.find_all(name, paths)
-
if templates.empty?
-
raise ActionView::MissingTemplate.new(paths, name, paths, false, "mailer")
-
else
-
templates.uniq(&:format).each(&block)
-
end
-
end
-
-
1
def wrap_inline_attachments(message)
-
# If we have both types of attachment, wrap all the inline attachments
-
# in multipart/related, but not the actual attachments
-
if message.attachments.detect(&:inline?) && message.attachments.detect { |a| !a.inline? }
-
related = Mail::Part.new
-
related.content_type = "multipart/related"
-
mixed = [ related ]
-
-
message.parts.each do |p|
-
if p.attachment? && !p.inline?
-
mixed << p
-
else
-
related.add_part(p)
-
end
-
end
-
-
message.parts.clear
-
mixed.each { |c| message.add_part(c) }
-
end
-
end
-
-
1
def create_parts_from_responses(m, responses)
-
if responses.size == 1 && !m.has_attachments?
-
responses[0].each { |k, v| m[k] = v }
-
elsif responses.size > 1 && m.has_attachments?
-
container = Mail::Part.new
-
container.content_type = "multipart/alternative"
-
responses.each { |r| insert_part(container, r, m.charset) }
-
m.add_part(container)
-
else
-
responses.each { |r| insert_part(m, r, m.charset) }
-
end
-
end
-
-
1
def insert_part(container, response, charset)
-
response[:charset] ||= charset
-
part = Mail::Part.new(response)
-
container.add_part(part)
-
end
-
-
# This and #instrument_name is for caching instrument
-
1
def instrument_payload(key)
-
{
-
mailer: mailer_name,
-
key: key
-
}
-
end
-
-
1
def instrument_name
-
"action_mailer"
-
end
-
-
1
def _protected_ivars
-
PROTECTED_IVARS
-
end
-
-
1
ActiveSupport.run_load_hooks(:action_mailer, self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "abstract_controller/collector"
-
1
require "active_support/core_ext/hash/reverse_merge"
-
1
require "active_support/core_ext/array/extract_options"
-
-
1
module ActionMailer
-
1
class Collector
-
1
include AbstractController::Collector
-
1
attr_reader :responses
-
-
1
def initialize(context, &block)
-
@context = context
-
@responses = []
-
@default_render = block
-
end
-
-
1
def any(*args, &block)
-
options = args.extract_options!
-
raise ArgumentError, "You have to supply at least one format" if args.empty?
-
args.each { |type| send(type, options.dup, &block) }
-
end
-
1
alias :all :any
-
-
1
def custom(mime, options = {})
-
options.reverse_merge!(content_type: mime.to_s)
-
@context.formats = [mime.to_sym]
-
options[:body] = block_given? ? yield : @default_render.call
-
@responses << options
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_job"
-
-
1
module ActionMailer
-
# The <tt>ActionMailer::DeliveryJob</tt> class is used when you
-
# want to send emails outside of the request-response cycle.
-
#
-
# Exceptions are rescued and handled by the mailer class.
-
1
class DeliveryJob < ActiveJob::Base # :nodoc:
-
1
queue_as { ActionMailer::Base.deliver_later_queue_name }
-
-
1
rescue_from StandardError, with: :handle_exception_with_mailer_class
-
-
1
before_perform do
-
ActiveSupport::Deprecation.warn <<~MSG.squish
-
Sending mail with DeliveryJob and Parameterized::DeliveryJob
-
is deprecated and will be removed in Rails 6.2.
-
Please use MailDeliveryJob instead.
-
MSG
-
end
-
-
1
def perform(mailer, mail_method, delivery_method, *args) #:nodoc:
-
mailer.constantize.public_send(mail_method, *args).send(delivery_method)
-
end
-
1
ruby2_keywords(:perform) if respond_to?(:ruby2_keywords, true)
-
-
1
private
-
# "Deserialize" the mailer class name by hand in case another argument
-
# (like a Global ID reference) raised DeserializationError.
-
1
def mailer_class
-
if mailer = Array(@serialized_arguments).first || Array(arguments).first
-
mailer.constantize
-
end
-
end
-
-
1
def handle_exception_with_mailer_class(exception)
-
if klass = mailer_class
-
klass.handle_exception exception
-
else
-
raise exception
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tmpdir"
-
-
1
module ActionMailer
-
# This module handles everything related to mail delivery, from registering
-
# new delivery methods to configuring the mail object to be sent.
-
1
module DeliveryMethods
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
# Do not make this inheritable, because we always want it to propagate
-
1
cattr_accessor :raise_delivery_errors, default: true
-
1
cattr_accessor :perform_deliveries, default: true
-
1
cattr_accessor :deliver_later_queue_name, default: :mailers
-
-
1
class_attribute :delivery_methods, default: {}.freeze
-
1
class_attribute :delivery_method, default: :smtp
-
-
1
add_delivery_method :smtp, Mail::SMTP,
-
address: "localhost",
-
port: 25,
-
domain: "localhost.localdomain",
-
user_name: nil,
-
password: nil,
-
authentication: nil,
-
enable_starttls_auto: true
-
-
1
add_delivery_method :file, Mail::FileDelivery,
-
1
location: defined?(Rails.root) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails"
-
-
1
add_delivery_method :sendmail, Mail::Sendmail,
-
location: "/usr/sbin/sendmail",
-
arguments: "-i"
-
-
1
add_delivery_method :test, Mail::TestMailer
-
end
-
-
# Helpers for creating and wrapping delivery behavior, used by DeliveryMethods.
-
1
module ClassMethods
-
# Provides a list of emails that have been delivered by Mail::TestMailer
-
1
delegate :deliveries, :deliveries=, to: Mail::TestMailer
-
-
# Adds a new delivery method through the given class using the given
-
# symbol as alias and the default options supplied.
-
#
-
# add_delivery_method :sendmail, Mail::Sendmail,
-
# location: '/usr/sbin/sendmail',
-
# arguments: '-i'
-
1
def add_delivery_method(symbol, klass, default_options = {})
-
4
class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
-
4
public_send(:"#{symbol}_settings=", default_options)
-
4
self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze
-
end
-
-
1
def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc:
-
method ||= delivery_method
-
mail.delivery_handler = self
-
-
case method
-
when NilClass
-
raise "Delivery method cannot be nil"
-
when Symbol
-
if klass = delivery_methods[method]
-
mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
-
else
-
raise "Invalid delivery method #{method.inspect}"
-
end
-
else
-
mail.delivery_method(method)
-
end
-
-
mail.perform_deliveries = perform_deliveries
-
mail.raise_delivery_errors = raise_delivery_errors
-
end
-
end
-
-
1
def wrap_delivery_behavior!(*args) # :nodoc:
-
self.class.wrap_delivery_behavior(message, *args)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "base64"
-
-
1
module ActionMailer
-
# Implements a mailer preview interceptor that converts image tag src attributes
-
# that use inline cid: style URLs to data: style URLs so that they are visible
-
# when previewing an HTML email in a web browser.
-
#
-
# This interceptor is enabled by default. To disable it, delete it from the
-
# <tt>ActionMailer::Base.preview_interceptors</tt> array:
-
#
-
# ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor)
-
#
-
1
class InlinePreviewInterceptor
-
1
PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i
-
-
1
include Base64
-
-
1
def self.previewing_email(message) #:nodoc:
-
new(message).transform!
-
end
-
-
1
def initialize(message) #:nodoc:
-
@message = message
-
end
-
-
1
def transform! #:nodoc:
-
return message if html_part.blank?
-
-
html_part.body = html_part.decoded.gsub(PATTERN) do |match|
-
if part = find_part(match[9..-2])
-
%[src="#{data_url(part)}"]
-
else
-
match
-
end
-
end
-
-
message
-
end
-
-
1
private
-
1
attr_reader :message
-
-
1
def html_part
-
@html_part ||= message.html_part
-
end
-
-
1
def data_url(part)
-
"data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}"
-
end
-
-
1
def find_part(cid)
-
message.all_parts.find { |p| p.attachment? && p.cid == cid }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/log_subscriber"
-
-
1
module ActionMailer
-
# Implements the ActiveSupport::LogSubscriber for logging notifications when
-
# email is delivered or received.
-
1
class LogSubscriber < ActiveSupport::LogSubscriber
-
# An email was delivered.
-
1
def deliver(event)
-
info do
-
perform_deliveries = event.payload[:perform_deliveries]
-
if perform_deliveries
-
"Delivered mail #{event.payload[:message_id]} (#{event.duration.round(1)}ms)"
-
else
-
"Skipped delivery of mail #{event.payload[:message_id]} as `perform_deliveries` is false"
-
end
-
end
-
-
debug { event.payload[:mail] }
-
end
-
-
# An email was generated.
-
1
def process(event)
-
debug do
-
mailer = event.payload[:mailer]
-
action = event.payload[:action]
-
"#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
-
end
-
end
-
-
# Use the logger configured for ActionMailer::Base.
-
1
def logger
-
ActionMailer::Base.logger
-
end
-
end
-
end
-
-
1
ActionMailer::LogSubscriber.attach_to :action_mailer
-
# frozen_string_literal: true
-
-
1
require "active_job"
-
-
1
module ActionMailer
-
# The <tt>ActionMailer::MailDeliveryJob</tt> class is used when you
-
# want to send emails outside of the request-response cycle. It supports
-
# sending either parameterized or normal mail.
-
#
-
# Exceptions are rescued and handled by the mailer class.
-
1
class MailDeliveryJob < ActiveJob::Base # :nodoc:
-
1
queue_as { ActionMailer::Base.deliver_later_queue_name }
-
-
1
rescue_from StandardError, with: :handle_exception_with_mailer_class
-
-
1
def perform(mailer, mail_method, delivery_method, args:, kwargs: nil, params: nil)
-
mailer_class = params ? mailer.constantize.with(params) : mailer.constantize
-
message = if kwargs
-
mailer_class.public_send(mail_method, *args, **kwargs)
-
else
-
mailer_class.public_send(mail_method, *args)
-
end
-
message.send(delivery_method)
-
end
-
-
1
private
-
# "Deserialize" the mailer class name by hand in case another argument
-
# (like a Global ID reference) raised DeserializationError.
-
1
def mailer_class
-
if mailer = Array(@serialized_arguments).first || Array(arguments).first
-
mailer.constantize
-
end
-
end
-
-
1
def handle_exception_with_mailer_class(exception)
-
if klass = mailer_class
-
klass.handle_exception exception
-
else
-
raise exception
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionMailer
-
# Provides helper methods for ActionMailer::Base that can be used for easily
-
# formatting messages, accessing mailer or message instances, and the
-
# attachments list.
-
1
module MailHelper
-
# Take the text and format it, indented two spaces for each line, and
-
# wrapped at 72 columns:
-
#
-
# text = <<-TEXT
-
# This is
-
# the paragraph.
-
#
-
# * item1 * item2
-
# TEXT
-
#
-
# block_format text
-
# # => " This is the paragraph.\n\n * item1\n * item2\n"
-
1
def block_format(text)
-
formatted = text.split(/\n\r?\n/).collect { |paragraph|
-
format_paragraph(paragraph)
-
}.join("\n\n")
-
-
# Make list points stand on their own line
-
formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" }
-
formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" }
-
-
formatted
-
end
-
-
# Access the mailer instance.
-
1
def mailer
-
@_controller
-
end
-
-
# Access the message instance.
-
1
def message
-
@_message
-
end
-
-
# Access the message attachments list.
-
1
def attachments
-
mailer.attachments
-
end
-
-
# Returns +text+ wrapped at +len+ columns and indented +indent+ spaces.
-
# By default column length +len+ equals 72 characters and indent
-
# +indent+ equal two spaces.
-
#
-
# my_text = 'Here is a sample text with more than 40 characters'
-
#
-
# format_paragraph(my_text, 25, 4)
-
# # => " Here is a sample text with\n more than 40 characters"
-
1
def format_paragraph(text, len = 72, indent = 2)
-
sentences = [[]]
-
-
text.split.each do |word|
-
if sentences.first.present? && (sentences.last + [word]).join(" ").length > len
-
sentences << [word]
-
else
-
sentences.last << word
-
end
-
end
-
-
indentation = " " * indent
-
sentences.map! { |sentence|
-
"#{indentation}#{sentence.join(' ')}"
-
}.join "\n"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "delegate"
-
-
1
module ActionMailer
-
# The <tt>ActionMailer::MessageDelivery</tt> class is used by
-
# ActionMailer::Base when creating a new mailer.
-
# <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
-
# created <tt>Mail::Message</tt>. You can get direct access to the
-
# <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
-
# through Active Job.
-
#
-
# Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object
-
# Notifier.welcome(User.first).deliver_now # sends the email
-
# Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
-
# Notifier.welcome(User.first).message # a Mail::Message object
-
1
class MessageDelivery < Delegator
-
1
def initialize(mailer_class, action, *args) #:nodoc:
-
@mailer_class, @action, @args = mailer_class, action, args
-
-
# The mail is only processed if we try to call any methods on it.
-
# Typical usage will leave it unloaded and call deliver_later.
-
@processed_mailer = nil
-
@mail_message = nil
-
end
-
1
ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
-
-
# Method calls are delegated to the Mail::Message that's ready to deliver.
-
1
def __getobj__ #:nodoc:
-
@mail_message ||= processed_mailer.message
-
end
-
-
# Unused except for delegator internals (dup, marshalling).
-
1
def __setobj__(mail_message) #:nodoc:
-
@mail_message = mail_message
-
end
-
-
# Returns the resulting Mail::Message
-
1
def message
-
__getobj__
-
end
-
-
# Was the delegate loaded, causing the mailer action to be processed?
-
1
def processed?
-
@processed_mailer || @mail_message
-
end
-
-
# Enqueues the email to be delivered through Active Job. When the
-
# job runs it will send the email using +deliver_now!+. That means
-
# that the message will be sent bypassing checking +perform_deliveries+
-
# and +raise_delivery_errors+, so use with caution.
-
#
-
# Notifier.welcome(User.first).deliver_later!
-
# Notifier.welcome(User.first).deliver_later!(wait: 1.hour)
-
# Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now)
-
# Notifier.welcome(User.first).deliver_later!(priority: 10)
-
#
-
# Options:
-
#
-
# * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
-
# * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
-
# * <tt>:queue</tt> - Enqueue the email on the specified queue
-
# * <tt>:priority</tt> - Enqueues the email with the specified priority
-
#
-
# By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
-
# <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
-
# +delivery_job+.
-
#
-
# class AccountRegistrationMailer < ApplicationMailer
-
# self.delivery_job = RegistrationDeliveryJob
-
# end
-
1
def deliver_later!(options = {})
-
enqueue_delivery :deliver_now!, options
-
end
-
-
# Enqueues the email to be delivered through Active Job. When the
-
# job runs it will send the email using +deliver_now+.
-
#
-
# Notifier.welcome(User.first).deliver_later
-
# Notifier.welcome(User.first).deliver_later(wait: 1.hour)
-
# Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now)
-
# Notifier.welcome(User.first).deliver_later(priority: 10)
-
#
-
# Options:
-
#
-
# * <tt>:wait</tt> - Enqueue the email to be delivered with a delay.
-
# * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time.
-
# * <tt>:queue</tt> - Enqueue the email on the specified queue.
-
# * <tt>:priority</tt> - Enqueues the email with the specified priority
-
#
-
# By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
-
# <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
-
# +delivery_job+.
-
#
-
# class AccountRegistrationMailer < ApplicationMailer
-
# self.delivery_job = RegistrationDeliveryJob
-
# end
-
1
def deliver_later(options = {})
-
enqueue_delivery :deliver_now, options
-
end
-
-
# Delivers an email without checking +perform_deliveries+ and +raise_delivery_errors+,
-
# so use with caution.
-
#
-
# Notifier.welcome(User.first).deliver_now!
-
#
-
1
def deliver_now!
-
processed_mailer.handle_exceptions do
-
message.deliver!
-
end
-
end
-
-
# Delivers an email:
-
#
-
# Notifier.welcome(User.first).deliver_now
-
#
-
1
def deliver_now
-
processed_mailer.handle_exceptions do
-
message.deliver
-
end
-
end
-
-
1
private
-
# Returns the processed Mailer instance. We keep this instance
-
# on hand so we can delegate exception handling to it.
-
1
def processed_mailer
-
@processed_mailer ||= @mailer_class.new.tap do |mailer|
-
mailer.process @action, *@args
-
end
-
end
-
-
1
def enqueue_delivery(delivery_method, options = {})
-
if processed?
-
::Kernel.raise "You've accessed the message before asking to " \
-
"deliver it later, so you may have made local changes that would " \
-
"be silently lost if we enqueued a job to deliver it. Why? Only " \
-
"the mailer method *arguments* are passed with the delivery job! " \
-
"Do not access the message in any way if you mean to deliver it " \
-
"later. Workarounds: 1. don't touch the message before calling " \
-
"#deliver_later, 2. only touch the message *within your mailer " \
-
"method*, or 3. use a custom Active Job instead of #deliver_later."
-
else
-
job = @mailer_class.delivery_job
-
-
if use_new_args?(job)
-
job.set(options).perform_later(
-
@mailer_class.name, @action.to_s, delivery_method.to_s, args: @args)
-
elsif job <= DeliveryJob
-
job.set(options).perform_later(
-
@mailer_class.name, @action.to_s, delivery_method.to_s, *@args)
-
else
-
ActiveSupport::Deprecation.warn(<<~EOM)
-
In Rails 6.2, Action Mailer will pass the mail arguments inside the `:args` keyword argument.
-
The `perform` method of the #{job} needs to change and forward the mail arguments
-
from the `args` keyword argument.
-
-
The `perform` method should now look like:
-
-
`def perform(mailer, mail_method, delivery, args:)`
-
EOM
-
-
job.set(options).perform_later(
-
@mailer_class.name, @action.to_s, delivery_method.to_s, *@args)
-
end
-
end
-
end
-
-
1
def use_new_args?(job)
-
parameters = job.public_instance_method(:perform).parameters
-
-
parameters.find do |key, name|
-
return true if key == :keyreq && name == :args
-
-
key == :keyrest
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionMailer
-
# Provides the option to parameterize mailers in order to share instance variable
-
# setup, processing, and common headers.
-
#
-
# Consider this example that does not use parameterization:
-
#
-
# class InvitationsMailer < ApplicationMailer
-
# def account_invitation(inviter, invitee)
-
# @account = inviter.account
-
# @inviter = inviter
-
# @invitee = invitee
-
#
-
# subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
-
#
-
# mail \
-
# subject: subject,
-
# to: invitee.email_address,
-
# from: common_address(inviter),
-
# reply_to: inviter.email_address_with_name
-
# end
-
#
-
# def project_invitation(project, inviter, invitee)
-
# @account = inviter.account
-
# @project = project
-
# @inviter = inviter
-
# @invitee = invitee
-
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
-
#
-
# subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
-
#
-
# mail \
-
# subject: subject,
-
# to: invitee.email_address,
-
# from: common_address(inviter),
-
# reply_to: inviter.email_address_with_name
-
# end
-
#
-
# def bulk_project_invitation(projects, inviter, invitee)
-
# @account = inviter.account
-
# @projects = projects.sort_by(&:name)
-
# @inviter = inviter
-
# @invitee = invitee
-
#
-
# subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
-
#
-
# mail \
-
# subject: subject,
-
# to: invitee.email_address,
-
# from: common_address(inviter),
-
# reply_to: inviter.email_address_with_name
-
# end
-
# end
-
#
-
# InvitationsMailer.account_invitation(person_a, person_b).deliver_later
-
#
-
# Using parameterized mailers, this can be rewritten as:
-
#
-
# class InvitationsMailer < ApplicationMailer
-
# before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
-
# before_action { @account = params[:inviter].account }
-
#
-
# default to: -> { @invitee.email_address },
-
# from: -> { common_address(@inviter) },
-
# reply_to: -> { @inviter.email_address_with_name }
-
#
-
# def account_invitation
-
# mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
-
# end
-
#
-
# def project_invitation
-
# @project = params[:project]
-
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
-
#
-
# mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
-
# end
-
#
-
# def bulk_project_invitation
-
# @projects = params[:projects].sort_by(&:name)
-
#
-
# mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
-
# end
-
# end
-
#
-
# InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
-
1
module Parameterized
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
attr_accessor :params
-
end
-
-
1
module ClassMethods
-
# Provide the parameters to the mailer in order to use them in the instance methods and callbacks.
-
#
-
# InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
-
#
-
# See Parameterized documentation for full example.
-
1
def with(params)
-
ActionMailer::Parameterized::Mailer.new(self, params)
-
end
-
end
-
-
1
class Mailer # :nodoc:
-
1
def initialize(mailer, params)
-
@mailer, @params = mailer, params
-
end
-
-
1
private
-
1
def method_missing(method_name, *args)
-
if @mailer.action_methods.include?(method_name.to_s)
-
ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args)
-
else
-
super
-
end
-
end
-
1
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
-
-
1
def respond_to_missing?(method, include_all = false)
-
@mailer.respond_to?(method, include_all)
-
end
-
end
-
-
1
class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
-
1
def perform(mailer, mail_method, delivery_method, params, *args)
-
mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
-
end
-
1
ruby2_keywords(:perform) if respond_to?(:ruby2_keywords, true)
-
end
-
-
1
class MessageDelivery < ActionMailer::MessageDelivery # :nodoc:
-
1
def initialize(mailer_class, action, params, *args)
-
super(mailer_class, action, *args)
-
@params = params
-
end
-
1
ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
-
-
1
private
-
1
def processed_mailer
-
@processed_mailer ||= @mailer_class.new.tap do |mailer|
-
mailer.params = @params
-
mailer.process @action, *@args
-
end
-
end
-
-
1
def enqueue_delivery(delivery_method, options = {})
-
if processed?
-
super
-
else
-
job = delivery_job_class
-
-
if job <= MailDeliveryJob
-
job.set(options).perform_later(
-
@mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args)
-
else
-
job.set(options).perform_later(
-
@mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args)
-
end
-
end
-
end
-
-
1
def delivery_job_class
-
if @mailer_class.delivery_job <= MailDeliveryJob
-
@mailer_class.delivery_job
-
else
-
Parameterized::DeliveryJob
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/descendants_tracker"
-
-
1
module ActionMailer
-
1
module Previews #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
# Set the location of mailer previews through app configuration:
-
#
-
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
-
#
-
1
mattr_accessor :preview_path, instance_writer: false
-
-
# Enable or disable mailer previews through app configuration:
-
#
-
# config.action_mailer.show_previews = true
-
#
-
# Defaults to +true+ for development environment
-
#
-
1
mattr_accessor :show_previews, instance_writer: false
-
-
# :nodoc:
-
1
mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]
-
end
-
-
1
module ClassMethods
-
# Register one or more Interceptors which will be called before mail is previewed.
-
1
def register_preview_interceptors(*interceptors)
-
1
interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
-
end
-
-
# Unregister one or more previously registered Interceptors.
-
1
def unregister_preview_interceptors(*interceptors)
-
interceptors.flatten.compact.each { |interceptor| unregister_preview_interceptor(interceptor) }
-
end
-
-
# Register an Interceptor which will be called before mail is previewed.
-
# Either a class or a string can be passed in as the Interceptor. If a
-
# string is passed in it will be constantized.
-
1
def register_preview_interceptor(interceptor)
-
preview_interceptor = interceptor_class_for(interceptor)
-
-
unless preview_interceptors.include?(preview_interceptor)
-
preview_interceptors << preview_interceptor
-
end
-
end
-
-
# Unregister a previously registered Interceptor.
-
# Either a class or a string can be passed in as the Interceptor. If a
-
# string is passed in it will be constantized.
-
1
def unregister_preview_interceptor(interceptor)
-
preview_interceptors.delete(interceptor_class_for(interceptor))
-
end
-
-
1
private
-
1
def interceptor_class_for(interceptor)
-
case interceptor
-
when String, Symbol
-
interceptor.to_s.camelize.constantize
-
else
-
interceptor
-
end
-
end
-
end
-
end
-
-
1
class Preview
-
1
extend ActiveSupport::DescendantsTracker
-
-
1
attr_reader :params
-
-
1
def initialize(params = {})
-
@params = params
-
end
-
-
1
class << self
-
# Returns all mailer preview classes.
-
1
def all
-
load_previews if descendants.empty?
-
descendants
-
end
-
-
# Returns the mail object for the given email name. The registered preview
-
# interceptors will be informed so that they can transform the message
-
# as they would if the mail was actually being delivered.
-
1
def call(email, params = {})
-
preview = new(params)
-
message = preview.public_send(email)
-
inform_preview_interceptors(message)
-
message
-
end
-
-
# Returns all of the available email previews.
-
1
def emails
-
public_instance_methods(false).map(&:to_s).sort
-
end
-
-
# Returns +true+ if the email exists.
-
1
def email_exists?(email)
-
emails.include?(email)
-
end
-
-
# Returns +true+ if the preview exists.
-
1
def exists?(preview)
-
all.any? { |p| p.preview_name == preview }
-
end
-
-
# Find a mailer preview by its underscored class name.
-
1
def find(preview)
-
all.find { |p| p.preview_name == preview }
-
end
-
-
# Returns the underscored name of the mailer preview without the suffix.
-
1
def preview_name
-
name.delete_suffix("Preview").underscore
-
end
-
-
1
private
-
1
def load_previews
-
if preview_path
-
Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
-
end
-
end
-
-
1
def preview_path
-
Base.preview_path
-
end
-
-
1
def show_previews
-
Base.show_previews
-
end
-
-
1
def inform_preview_interceptors(message)
-
Base.preview_interceptors.each do |interceptor|
-
interceptor.previewing_email(message)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionMailer #:nodoc:
-
# Provides +rescue_from+ for mailers. Wraps mailer action processing,
-
# mail job processing, and mail delivery.
-
1
module Rescuable
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Rescuable
-
-
1
class_methods do
-
1
def handle_exception(exception) #:nodoc:
-
rescue_with_handler(exception) || raise(exception)
-
end
-
end
-
-
1
def handle_exceptions #:nodoc:
-
yield
-
rescue => exception
-
rescue_with_handler(exception) || raise
-
end
-
-
1
private
-
1
def process(*)
-
handle_exceptions do
-
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/test_case"
-
1
require "rails-dom-testing"
-
-
1
module ActionMailer
-
1
class NonInferrableMailerError < ::StandardError
-
1
def initialize(name)
-
super "Unable to determine the mailer to test from #{name}. " \
-
"You'll need to specify it using tests YourMailer in your " \
-
"test case definition"
-
end
-
end
-
-
1
class TestCase < ActiveSupport::TestCase
-
1
module ClearTestDeliveries
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
setup :clear_test_deliveries
-
1
teardown :clear_test_deliveries
-
end
-
-
1
private
-
1
def clear_test_deliveries
-
if ActionMailer::Base.delivery_method == :test
-
ActionMailer::Base.deliveries.clear
-
end
-
end
-
end
-
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
-
1
include ActiveSupport::Testing::ConstantLookup
-
1
include TestHelper
-
1
include Rails::Dom::Testing::Assertions::SelectorAssertions
-
1
include Rails::Dom::Testing::Assertions::DomAssertions
-
-
1
included do
-
1
class_attribute :_mailer_class
-
1
setup :initialize_test_deliveries
-
1
setup :set_expected_mail
-
1
teardown :restore_test_deliveries
-
1
ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
-
end
-
-
1
module ClassMethods
-
1
def tests(mailer)
-
case mailer
-
when String, Symbol
-
self._mailer_class = mailer.to_s.camelize.constantize
-
when Module
-
self._mailer_class = mailer
-
else
-
raise NonInferrableMailerError.new(mailer)
-
end
-
end
-
-
1
def mailer_class
-
if mailer = _mailer_class
-
mailer
-
else
-
tests determine_default_mailer(name)
-
end
-
end
-
-
1
def determine_default_mailer(name)
-
mailer = determine_constant_from_test_name(name) do |constant|
-
Class === constant && constant < ActionMailer::Base
-
end
-
raise NonInferrableMailerError.new(name) if mailer.nil?
-
mailer
-
end
-
end
-
-
1
private
-
1
def initialize_test_deliveries
-
set_delivery_method :test
-
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
-
ActionMailer::Base.perform_deliveries = true
-
ActionMailer::Base.deliveries.clear
-
end
-
-
1
def restore_test_deliveries
-
restore_delivery_method
-
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
-
end
-
-
1
def set_delivery_method(method)
-
@old_delivery_method = ActionMailer::Base.delivery_method
-
ActionMailer::Base.delivery_method = method
-
end
-
-
1
def restore_delivery_method
-
ActionMailer::Base.deliveries.clear
-
ActionMailer::Base.delivery_method = @old_delivery_method
-
end
-
-
1
def set_expected_mail
-
@expected = Mail.new
-
@expected.content_type ["text", "plain", { "charset" => charset }]
-
@expected.mime_version = "1.0"
-
end
-
-
1
def charset
-
"UTF-8"
-
end
-
-
1
def encode(subject)
-
Mail::Encodings.q_value_encode(subject, charset)
-
end
-
-
1
def read_fixture(action)
-
IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_job"
-
-
1
module ActionMailer
-
# Provides helper methods for testing Action Mailer, including #assert_emails
-
# and #assert_no_emails.
-
1
module TestHelper
-
1
include ActiveJob::TestHelper
-
-
# Asserts that the number of emails sent matches the given number.
-
#
-
# def test_emails
-
# assert_emails 0
-
# ContactMailer.welcome.deliver_now
-
# assert_emails 1
-
# ContactMailer.welcome.deliver_now
-
# assert_emails 2
-
# end
-
#
-
# If a block is passed, that block should cause the specified number of
-
# emails to be sent.
-
#
-
# def test_emails_again
-
# assert_emails 1 do
-
# ContactMailer.welcome.deliver_now
-
# end
-
#
-
# assert_emails 2 do
-
# ContactMailer.welcome.deliver_now
-
# ContactMailer.welcome.deliver_later
-
# end
-
# end
-
1
def assert_emails(number, &block)
-
if block_given?
-
original_count = ActionMailer::Base.deliveries.size
-
perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, &block)
-
new_count = ActionMailer::Base.deliveries.size
-
assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent"
-
else
-
assert_equal number, ActionMailer::Base.deliveries.size
-
end
-
end
-
-
# Asserts that no emails have been sent.
-
#
-
# def test_emails
-
# assert_no_emails
-
# ContactMailer.welcome.deliver_now
-
# assert_emails 1
-
# end
-
#
-
# If a block is passed, that block should not cause any emails to be sent.
-
#
-
# def test_emails_again
-
# assert_no_emails do
-
# # No emails should be sent from this block
-
# end
-
# end
-
#
-
# Note: This assertion is simply a shortcut for:
-
#
-
# assert_emails 0, &block
-
1
def assert_no_emails(&block)
-
assert_emails 0, &block
-
end
-
-
# Asserts that the number of emails enqueued for later delivery matches
-
# the given number.
-
#
-
# def test_emails
-
# assert_enqueued_emails 0
-
# ContactMailer.welcome.deliver_later
-
# assert_enqueued_emails 1
-
# ContactMailer.welcome.deliver_later
-
# assert_enqueued_emails 2
-
# end
-
#
-
# If a block is passed, that block should cause the specified number of
-
# emails to be enqueued.
-
#
-
# def test_emails_again
-
# assert_enqueued_emails 1 do
-
# ContactMailer.welcome.deliver_later
-
# end
-
#
-
# assert_enqueued_emails 2 do
-
# ContactMailer.welcome.deliver_later
-
# ContactMailer.welcome.deliver_later
-
# end
-
# end
-
1
def assert_enqueued_emails(number, &block)
-
assert_enqueued_jobs(number, only: ->(job) { delivery_job_filter(job) }, &block)
-
end
-
-
# Asserts that a specific email has been enqueued, optionally
-
# matching arguments.
-
#
-
# def test_email
-
# ContactMailer.welcome.deliver_later
-
# assert_enqueued_email_with ContactMailer, :welcome
-
# end
-
#
-
# def test_email_with_arguments
-
# ContactMailer.welcome("Hello", "Goodbye").deliver_later
-
# assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"]
-
# end
-
#
-
# If a block is passed, that block should cause the specified email
-
# to be enqueued.
-
#
-
# def test_email_in_block
-
# assert_enqueued_email_with ContactMailer, :welcome do
-
# ContactMailer.welcome.deliver_later
-
# end
-
# end
-
#
-
# If +args+ is provided as a Hash, a parameterized email is matched.
-
#
-
# def test_parameterized_email
-
# assert_enqueued_email_with ContactMailer, :welcome,
-
# args: {email: 'user@example.com'} do
-
# ContactMailer.with(email: 'user@example.com').welcome.deliver_later
-
# end
-
# end
-
1
def assert_enqueued_email_with(mailer, method, args: nil, queue: ActionMailer::Base.deliver_later_queue_name || "default", &block)
-
args = if args.is_a?(Hash)
-
[mailer.to_s, method.to_s, "deliver_now", params: args, args: []]
-
else
-
[mailer.to_s, method.to_s, "deliver_now", args: Array(args)]
-
end
-
assert_enqueued_with(job: mailer.delivery_job, args: args, queue: queue.to_s, &block)
-
end
-
-
# Asserts that no emails are enqueued for later delivery.
-
#
-
# def test_no_emails
-
# assert_no_enqueued_emails
-
# ContactMailer.welcome.deliver_later
-
# assert_enqueued_emails 1
-
# end
-
#
-
# If a block is provided, it should not cause any emails to be enqueued.
-
#
-
# def test_no_emails
-
# assert_no_enqueued_emails do
-
# # No emails should be enqueued from this block
-
# end
-
# end
-
1
def assert_no_enqueued_emails(&block)
-
assert_enqueued_emails 0, &block
-
end
-
-
1
private
-
1
def delivery_job_filter(job)
-
job_class = job.is_a?(Hash) ? job.fetch(:job) : job.class
-
-
Base.descendants.map(&:delivery_job).include?(job_class) ||
-
ActionMailer::Parameterized::DeliveryJob == job_class
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module AbstractController
-
1
module AssetPaths #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
2
config_accessor :asset_host, :assets_dir, :javascripts_dir,
-
:stylesheets_dir, :default_asset_host_protocol, :relative_url_root
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "abstract_controller/error"
-
1
require "active_support/configurable"
-
1
require "active_support/descendants_tracker"
-
1
require "active_support/core_ext/module/anonymous"
-
1
require "active_support/core_ext/module/attr_internal"
-
-
1
module AbstractController
-
# Raised when a non-existing controller action is triggered.
-
1
class ActionNotFound < StandardError
-
1
attr_reader :controller, :action
-
1
def initialize(message = nil, controller = nil, action = nil)
-
@controller = controller
-
@action = action
-
super(message)
-
end
-
-
1
class Correction
-
1
def initialize(error)
-
@error = error
-
end
-
-
1
def corrections
-
if @error.action
-
maybe_these = @error.controller.class.action_methods
-
-
maybe_these.sort_by { |n|
-
DidYouMean::Jaro.distance(@error.action.to_s, n)
-
}.reverse.first(4)
-
else
-
[]
-
end
-
end
-
end
-
-
# We may not have DYM, and DYM might not let us register error handlers
-
1
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
-
DidYouMean.correct_error(self, Correction)
-
end
-
end
-
-
# AbstractController::Base is a low-level API. Nobody should be
-
# using it directly, and subclasses (like ActionController::Base) are
-
# expected to provide their own +render+ method, since rendering means
-
# different things depending on the context.
-
1
class Base
-
##
-
# Returns the body of the HTTP response sent by the controller.
-
1
attr_internal :response_body
-
-
##
-
# Returns the name of the action this controller is processing.
-
1
attr_internal :action_name
-
-
##
-
# Returns the formats that can be processed by the controller.
-
1
attr_internal :formats
-
-
1
include ActiveSupport::Configurable
-
1
extend ActiveSupport::DescendantsTracker
-
-
1
class << self
-
1
attr_reader :abstract
-
1
alias_method :abstract?, :abstract
-
-
# Define a controller as abstract. See internal_methods for more
-
# details.
-
1
def abstract!
-
4
@abstract = true
-
end
-
-
1
def inherited(klass) # :nodoc:
-
# Define the abstract ivar on subclasses so that we don't get
-
# uninitialized ivar warnings
-
4
unless klass.instance_variable_defined?(:@abstract)
-
4
klass.instance_variable_set(:@abstract, false)
-
end
-
4
super
-
end
-
-
# A list of all internal methods for a controller. This finds the first
-
# abstract superclass of a controller, and gets a list of all public
-
# instance methods on that abstract class. Public instance methods of
-
# a controller would normally be considered action methods, so methods
-
# declared on abstract classes are being removed.
-
# (<tt>ActionController::Metal</tt> and ActionController::Base are defined as abstract)
-
1
def internal_methods
-
1
controller = self
-
-
1
controller = controller.superclass until controller.abstract?
-
1
controller.public_instance_methods(true)
-
end
-
-
# A list of method names that should be considered actions. This
-
# includes all public instance methods on a controller, less
-
# any internal methods (see internal_methods), adding back in
-
# any methods that are internal, but still exist on the class
-
# itself.
-
#
-
# ==== Returns
-
# * <tt>Set</tt> - A set of all methods that should be considered actions.
-
1
def action_methods
-
1
@action_methods ||= begin
-
# All public instance methods of this class, including ancestors
-
1
methods = (public_instance_methods(true) -
-
# Except for public instance methods of Base and its ancestors
-
internal_methods +
-
# Be sure to include shadowed public instance methods of this class
-
public_instance_methods(false))
-
-
1
methods.map!(&:to_s)
-
-
1
methods.to_set
-
end
-
end
-
-
# action_methods are cached and there is sometimes a need to refresh
-
# them. ::clear_action_methods! allows you to do that, so next time
-
# you run action_methods, they will be recalculated.
-
1
def clear_action_methods!
-
291
@action_methods = nil
-
end
-
-
# Returns the full controller name, underscored, without the ending Controller.
-
#
-
# class MyApp::MyPostsController < AbstractController::Base
-
#
-
# end
-
#
-
# MyApp::MyPostsController.controller_path # => "my_app/my_posts"
-
#
-
# ==== Returns
-
# * <tt>String</tt>
-
1
def controller_path
-
6
@controller_path ||= name.delete_suffix("Controller").underscore unless anonymous?
-
end
-
-
# Refresh the cached action_methods when a new action_method is added.
-
1
def method_added(name)
-
291
super
-
291
clear_action_methods!
-
end
-
end
-
-
1
abstract!
-
-
# Calls the action going through the entire action dispatch stack.
-
#
-
# The actual method that is called is determined by calling
-
# #method_for_action. If no method can handle the action, then an
-
# AbstractController::ActionNotFound error is raised.
-
#
-
# ==== Returns
-
# * <tt>self</tt>
-
1
def process(action, *args)
-
@_action_name = action.to_s
-
-
unless action_name = _find_action_name(@_action_name)
-
raise ActionNotFound.new("The action '#{action}' could not be found for #{self.class.name}", self, action)
-
end
-
-
@_response_body = nil
-
-
process_action(action_name, *args)
-
end
-
-
# Delegates to the class' ::controller_path
-
1
def controller_path
-
self.class.controller_path
-
end
-
-
# Delegates to the class' ::action_methods
-
1
def action_methods
-
self.class.action_methods
-
end
-
-
# Returns true if a method for the action is available and
-
# can be dispatched, false otherwise.
-
#
-
# Notice that <tt>action_methods.include?("foo")</tt> may return
-
# false and <tt>available_action?("foo")</tt> returns true because
-
# this method considers actions that are also available
-
# through other means, for example, implicit render ones.
-
#
-
# ==== Parameters
-
# * <tt>action_name</tt> - The name of an action to be tested
-
1
def available_action?(action_name)
-
_find_action_name(action_name)
-
end
-
-
# Tests if a response body is set. Used to determine if the
-
# +process_action+ callback needs to be terminated in
-
# +AbstractController::Callbacks+.
-
1
def performed?
-
response_body
-
end
-
-
# Returns true if the given controller is capable of rendering
-
# a path. A subclass of +AbstractController::Base+
-
# may return false. An Email controller for example does not
-
# support paths, only full URLs.
-
1
def self.supports_path?
-
true
-
end
-
-
1
def inspect # :nodoc:
-
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
-
end
-
-
1
private
-
# Returns true if the name can be considered an action because
-
# it has a method defined in the controller.
-
#
-
# ==== Parameters
-
# * <tt>name</tt> - The name of an action to be tested
-
1
def action_method?(name)
-
self.class.action_methods.include?(name)
-
end
-
-
# Call the action. Override this in a subclass to modify the
-
# behavior around processing an action. This, and not #process,
-
# is the intended way to override action dispatching.
-
#
-
# Notice that the first argument is the method to be dispatched
-
# which is *not* necessarily the same as the action name.
-
1
def process_action(method_name, *args)
-
send_action(method_name, *args)
-
end
-
-
# Actually call the method associated with the action. Override
-
# this method if you wish to change how action methods are called,
-
# not to add additional behavior around it. For example, you would
-
# override #send_action if you want to inject arguments into the
-
# method.
-
1
alias send_action send
-
-
# If the action name was not found, but a method called "action_missing"
-
# was found, #method_for_action will return "_handle_action_missing".
-
# This method calls #action_missing with the current action name.
-
1
def _handle_action_missing(*args)
-
action_missing(@_action_name, *args)
-
end
-
-
# Takes an action name and returns the name of the method that will
-
# handle the action.
-
#
-
# It checks if the action name is valid and returns false otherwise.
-
#
-
# See method_for_action for more information.
-
#
-
# ==== Parameters
-
# * <tt>action_name</tt> - An action name to find a method name for
-
#
-
# ==== Returns
-
# * <tt>string</tt> - The name of the method that handles the action
-
# * false - No valid method name could be found.
-
# Raise +AbstractController::ActionNotFound+.
-
1
def _find_action_name(action_name)
-
_valid_action_name?(action_name) && method_for_action(action_name)
-
end
-
-
# Takes an action name and returns the name of the method that will
-
# handle the action. In normal cases, this method returns the same
-
# name as it receives. By default, if #method_for_action receives
-
# a name that is not an action, it will look for an #action_missing
-
# method and return "_handle_action_missing" if one is found.
-
#
-
# Subclasses may override this method to add additional conditions
-
# that should be considered an action. For instance, an HTTP controller
-
# with a template matching the action name is considered to exist.
-
#
-
# If you override this method to handle additional cases, you may
-
# also provide a method (like +_handle_method_missing+) to handle
-
# the case.
-
#
-
# If none of these conditions are true, and +method_for_action+
-
# returns +nil+, an +AbstractController::ActionNotFound+ exception will be raised.
-
#
-
# ==== Parameters
-
# * <tt>action_name</tt> - An action name to find a method name for
-
#
-
# ==== Returns
-
# * <tt>string</tt> - The name of the method that handles the action
-
# * <tt>nil</tt> - No method name could be found.
-
1
def method_for_action(action_name)
-
if action_method?(action_name)
-
action_name
-
elsif respond_to?(:action_missing, true)
-
"_handle_action_missing"
-
end
-
end
-
-
# Checks if the action name is valid and returns false otherwise.
-
1
def _valid_action_name?(action_name)
-
!action_name.to_s.include? File::SEPARATOR
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module AbstractController
-
1
module Caching
-
1
extend ActiveSupport::Concern
-
1
extend ActiveSupport::Autoload
-
-
1
eager_autoload do
-
1
autoload :Fragments
-
end
-
-
1
module ConfigMethods
-
1
def cache_store
-
config.cache_store
-
end
-
-
1
def cache_store=(store)
-
2
config.cache_store = ActiveSupport::Cache.lookup_store(*store)
-
end
-
-
1
private
-
1
def cache_configured?
-
perform_caching && cache_store
-
end
-
end
-
-
1
include ConfigMethods
-
1
include AbstractController::Caching::Fragments
-
-
1
included do
-
2
extend ConfigMethods
-
-
2
config_accessor :default_static_extension
-
2
self.default_static_extension ||= ".html"
-
-
2
config_accessor :perform_caching
-
2
self.perform_caching = true if perform_caching.nil?
-
-
2
config_accessor :enable_fragment_cache_logging
-
2
self.enable_fragment_cache_logging = false
-
-
2
class_attribute :_view_cache_dependencies, default: []
-
2
helper_method :view_cache_dependencies if respond_to?(:helper_method)
-
end
-
-
1
module ClassMethods
-
1
def view_cache_dependency(&dependency)
-
self._view_cache_dependencies += [dependency]
-
end
-
end
-
-
1
def view_cache_dependencies
-
self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
-
end
-
-
1
private
-
# Convenience accessor.
-
1
def cache(key, options = {}, &block) # :doc:
-
if cache_configured?
-
cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
-
else
-
yield
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module AbstractController
-
1
module Caching
-
# Fragment caching is used for caching various blocks within
-
# views without caching the entire action as a whole. This is
-
# useful when certain elements of an action change frequently or
-
# depend on complicated state while other parts rarely change or
-
# can be shared amongst multiple parties. The caching is done using
-
# the +cache+ helper available in the Action View. See
-
# ActionView::Helpers::CacheHelper for more information.
-
#
-
# While it's strongly recommended that you use key-based cache
-
# expiration (see links in CacheHelper for more information),
-
# it is also possible to manually expire caches. For example:
-
#
-
# expire_fragment('name_of_cache')
-
1
module Fragments
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
2
if respond_to?(:class_attribute)
-
2
class_attribute :fragment_cache_keys
-
else
-
mattr_writer :fragment_cache_keys
-
end
-
-
2
self.fragment_cache_keys = []
-
-
2
if respond_to?(:helper_method)
-
2
helper_method :combined_fragment_cache_key
-
end
-
end
-
-
1
module ClassMethods
-
# Allows you to specify controller-wide key prefixes for
-
# cache fragments. Pass either a constant +value+, or a block
-
# which computes a value each time a cache key is generated.
-
#
-
# For example, you may want to prefix all fragment cache keys
-
# with a global version identifier, so you can easily
-
# invalidate all caches.
-
#
-
# class ApplicationController
-
# fragment_cache_key "v1"
-
# end
-
#
-
# When it's time to invalidate all fragments, simply change
-
# the string constant. Or, progressively roll out the cache
-
# invalidation using a computed value:
-
#
-
# class ApplicationController
-
# fragment_cache_key do
-
# @account.id.odd? ? "v1" : "v2"
-
# end
-
# end
-
1
def fragment_cache_key(value = nil, &key)
-
self.fragment_cache_keys += [key || -> { value }]
-
end
-
end
-
-
# Given a key (as described in +expire_fragment+), returns
-
# a key array suitable for use in reading, writing, or expiring a
-
# cached fragment. All keys begin with <tt>:views</tt>,
-
# followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set,
-
# followed by any controller-wide key prefix values, ending
-
# with the specified +key+ value.
-
1
def combined_fragment_cache_key(key)
-
head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
-
tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
-
-
cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail]
-
cache_key.flatten!(1)
-
cache_key.compact!
-
cache_key
-
end
-
-
# Writes +content+ to the location signified by
-
# +key+ (see +expire_fragment+ for acceptable formats).
-
1
def write_fragment(key, content, options = nil)
-
return content unless cache_configured?
-
-
key = combined_fragment_cache_key(key)
-
instrument_fragment_cache :write_fragment, key do
-
content = content.to_str
-
cache_store.write(key, content, options)
-
end
-
content
-
end
-
-
# Reads a cached fragment from the location signified by +key+
-
# (see +expire_fragment+ for acceptable formats).
-
1
def read_fragment(key, options = nil)
-
return unless cache_configured?
-
-
key = combined_fragment_cache_key(key)
-
instrument_fragment_cache :read_fragment, key do
-
result = cache_store.read(key, options)
-
result.respond_to?(:html_safe) ? result.html_safe : result
-
end
-
end
-
-
# Check if a cached fragment from the location signified by
-
# +key+ exists (see +expire_fragment+ for acceptable formats).
-
1
def fragment_exist?(key, options = nil)
-
return unless cache_configured?
-
key = combined_fragment_cache_key(key)
-
-
instrument_fragment_cache :exist_fragment?, key do
-
cache_store.exist?(key, options)
-
end
-
end
-
-
# Removes fragments from the cache.
-
#
-
# +key+ can take one of three forms:
-
#
-
# * String - This would normally take the form of a path, like
-
# <tt>pages/45/notes</tt>.
-
# * Hash - Treated as an implicit call to +url_for+, like
-
# <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
-
# * Regexp - Will remove any fragment that matches, so
-
# <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
-
# don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
-
# the actual filename matched looks like
-
# <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
-
# only supported on caches that can iterate over all keys (unlike
-
# memcached).
-
#
-
# +options+ is passed through to the cache store's +delete+
-
# method (or <tt>delete_matched</tt>, for Regexp keys).
-
1
def expire_fragment(key, options = nil)
-
return unless cache_configured?
-
key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)
-
-
instrument_fragment_cache :expire_fragment, key do
-
if key.is_a?(Regexp)
-
cache_store.delete_matched(key, options)
-
else
-
cache_store.delete(key, options)
-
end
-
end
-
end
-
-
1
def instrument_fragment_cache(name, key) # :nodoc:
-
ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module AbstractController
-
# = Abstract Controller Callbacks
-
#
-
# Abstract Controller provides hooks during the life cycle of a controller action.
-
# Callbacks allow you to trigger logic during this cycle. Available callbacks are:
-
#
-
# * <tt>after_action</tt>
-
# * <tt>append_after_action</tt>
-
# * <tt>append_around_action</tt>
-
# * <tt>append_before_action</tt>
-
# * <tt>around_action</tt>
-
# * <tt>before_action</tt>
-
# * <tt>prepend_after_action</tt>
-
# * <tt>prepend_around_action</tt>
-
# * <tt>prepend_before_action</tt>
-
# * <tt>skip_after_action</tt>
-
# * <tt>skip_around_action</tt>
-
# * <tt>skip_before_action</tt>
-
#
-
# NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
-
#
-
1
module Callbacks
-
1
extend ActiveSupport::Concern
-
-
# Uses ActiveSupport::Callbacks as the base functionality. For
-
# more details on the whole callback system, read the documentation
-
# for ActiveSupport::Callbacks.
-
1
include ActiveSupport::Callbacks
-
-
1
included do
-
2
define_callbacks :process_action,
-
terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
-
skip_after_callbacks_if_terminated: true
-
end
-
-
# Override <tt>AbstractController::Base#process_action</tt> to run the
-
# <tt>process_action</tt> callbacks around the normal behavior.
-
1
def process_action(*)
-
run_callbacks(:process_action) do
-
super
-
end
-
end
-
-
1
module ClassMethods
-
# If +:only+ or +:except+ are used, convert the options into the
-
# +:if+ and +:unless+ options of ActiveSupport::Callbacks.
-
#
-
# The basic idea is that <tt>:only => :index</tt> gets converted to
-
# <tt>:if => proc {|c| c.action_name == "index" }</tt>.
-
#
-
# Note that <tt>:only</tt> has priority over <tt>:if</tt> in case they
-
# are used together.
-
#
-
# only: :index, if: -> { true } # the :if option will be ignored.
-
#
-
# Note that <tt>:if</tt> has priority over <tt>:except</tt> in case they
-
# are used together.
-
#
-
# except: :index, if: -> { true } # the :except option will be ignored.
-
#
-
# ==== Options
-
# * <tt>only</tt> - The callback should be run only for this action.
-
# * <tt>except</tt> - The callback should be run for all actions except this action.
-
1
def _normalize_callback_options(options)
-
4
_normalize_callback_option(options, :only, :if)
-
4
_normalize_callback_option(options, :except, :unless)
-
end
-
-
1
def _normalize_callback_option(options, from, to) # :nodoc:
-
8
if from = options.delete(from)
-
_from = Array(from).map(&:to_s).to_set
-
from = proc { |c| _from.include? c.action_name }
-
options[to] = Array(options[to]).unshift(from)
-
end
-
end
-
-
# Take callback names and an optional callback proc, normalize them,
-
# then call the block with each callback. This allows us to abstract
-
# the normalization across several methods that use it.
-
#
-
# ==== Parameters
-
# * <tt>callbacks</tt> - An array of callbacks, with an optional
-
# options hash as the last parameter.
-
# * <tt>block</tt> - A proc that should be added to the callbacks.
-
#
-
# ==== Block Parameters
-
# * <tt>name</tt> - The callback to be added.
-
# * <tt>options</tt> - A hash of options to be used when adding the callback.
-
1
def _insert_callbacks(callbacks, block = nil)
-
4
options = callbacks.extract_options!
-
4
_normalize_callback_options(options)
-
4
callbacks.push(block) if block
-
4
callbacks.each do |callback|
-
4
yield callback, options
-
end
-
end
-
-
##
-
# :method: before_action
-
#
-
# :call-seq: before_action(names, block)
-
#
-
# Append a callback before actions. See _insert_callbacks for parameter details.
-
#
-
# If the callback renders or redirects, the action will not run. If there
-
# are additional callbacks scheduled to run after that callback, they are
-
# also cancelled.
-
-
##
-
# :method: prepend_before_action
-
#
-
# :call-seq: prepend_before_action(names, block)
-
#
-
# Prepend a callback before actions. See _insert_callbacks for parameter details.
-
#
-
# If the callback renders or redirects, the action will not run. If there
-
# are additional callbacks scheduled to run after that callback, they are
-
# also cancelled.
-
-
##
-
# :method: skip_before_action
-
#
-
# :call-seq: skip_before_action(names)
-
#
-
# Skip a callback before actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: append_before_action
-
#
-
# :call-seq: append_before_action(names, block)
-
#
-
# Append a callback before actions. See _insert_callbacks for parameter details.
-
#
-
# If the callback renders or redirects, the action will not run. If there
-
# are additional callbacks scheduled to run after that callback, they are
-
# also cancelled.
-
-
##
-
# :method: after_action
-
#
-
# :call-seq: after_action(names, block)
-
#
-
# Append a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: prepend_after_action
-
#
-
# :call-seq: prepend_after_action(names, block)
-
#
-
# Prepend a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: skip_after_action
-
#
-
# :call-seq: skip_after_action(names)
-
#
-
# Skip a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: append_after_action
-
#
-
# :call-seq: append_after_action(names, block)
-
#
-
# Append a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: around_action
-
#
-
# :call-seq: around_action(names, block)
-
#
-
# Append a callback around actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: prepend_around_action
-
#
-
# :call-seq: prepend_around_action(names, block)
-
#
-
# Prepend a callback around actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: skip_around_action
-
#
-
# :call-seq: skip_around_action(names)
-
#
-
# Skip a callback around actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: append_around_action
-
#
-
# :call-seq: append_around_action(names, block)
-
#
-
# Append a callback around actions. See _insert_callbacks for parameter details.
-
-
# set up before_action, prepend_before_action, skip_before_action, etc.
-
# for each of before, after, and around.
-
1
[:before, :after, :around].each do |callback|
-
3
define_method "#{callback}_action" do |*names, &blk|
-
4
_insert_callbacks(names, blk) do |name, options|
-
4
set_callback(:process_action, callback, name, options)
-
end
-
end
-
-
3
define_method "prepend_#{callback}_action" do |*names, &blk|
-
_insert_callbacks(names, blk) do |name, options|
-
set_callback(:process_action, callback, name, options.merge(prepend: true))
-
end
-
end
-
-
# Skip a before, after or around callback. See _insert_callbacks
-
# for details on the allowed parameters.
-
3
define_method "skip_#{callback}_action" do |*names|
-
_insert_callbacks(names) do |name, options|
-
skip_callback(:process_action, callback, name, options)
-
end
-
end
-
-
# *_action is the same as append_*_action
-
3
alias_method :"append_#{callback}_action", :"#{callback}_action"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/http/mime_type"
-
-
1
module AbstractController
-
1
module Collector
-
1
def self.generate_method_for_mime(mime)
-
34
sym = mime.is_a?(Symbol) ? mime : mime.to_sym
-
34
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{sym}(*args, &block)
-
custom(Mime[:#{sym}], *args, &block)
-
end
-
RUBY
-
end
-
-
1
Mime::SET.each do |mime|
-
34
generate_method_for_mime(mime)
-
end
-
-
1
Mime::Type.register_callback do |mime|
-
generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym)
-
end
-
-
1
private
-
1
def method_missing(symbol, &block)
-
unless mime_constant = Mime[symbol]
-
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
-
"https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
-
"If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
-
"be sure to nest your variant response within a format response: " \
-
"format.html { |html| html.tablet { ... } }"
-
end
-
-
if Mime::SET.include?(mime_constant)
-
AbstractController::Collector.generate_method_for_mime(mime_constant)
-
send(symbol, &block)
-
else
-
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module AbstractController
-
1
class Error < StandardError #:nodoc:
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/benchmarkable"
-
-
1
module AbstractController
-
1
module Logger #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
2
config_accessor :logger
-
2
include ActiveSupport::Benchmarkable
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "abstract_controller/error"
-
1
require "action_view"
-
1
require "action_view/view_paths"
-
1
require "set"
-
-
1
module AbstractController
-
1
class DoubleRenderError < Error
-
1
DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"."
-
-
1
def initialize(message = nil)
-
super(message || DEFAULT_MESSAGE)
-
end
-
end
-
-
1
module Rendering
-
1
extend ActiveSupport::Concern
-
1
include ActionView::ViewPaths
-
-
# Normalizes arguments, options and then delegates render_to_body and
-
# sticks the result in <tt>self.response_body</tt>.
-
1
def render(*args, &block)
-
options = _normalize_render(*args, &block)
-
rendered_body = render_to_body(options)
-
if options[:html]
-
_set_html_content_type
-
else
-
_set_rendered_content_type rendered_format
-
end
-
_set_vary_header
-
self.response_body = rendered_body
-
end
-
-
# Raw rendering of a template to a string.
-
#
-
# It is similar to render, except that it does not
-
# set the +response_body+ and it should be guaranteed
-
# to always return a string.
-
#
-
# If a component extends the semantics of +response_body+
-
# (as ActionController extends it to be anything that
-
# responds to the method each), this method needs to be
-
# overridden in order to still return a string.
-
1
def render_to_string(*args, &block)
-
options = _normalize_render(*args, &block)
-
render_to_body(options)
-
end
-
-
# Performs the actual template rendering.
-
1
def render_to_body(options = {})
-
end
-
-
# Returns Content-Type of rendered content.
-
1
def rendered_format
-
Mime[:text]
-
end
-
-
1
DEFAULT_PROTECTED_INSTANCE_VARIABLES = %i(@_action_name @_response_body @_formats @_prefixes)
-
-
# This method should return a hash with assigns.
-
# You can overwrite this configuration per controller.
-
1
def view_assigns
-
variables = instance_variables - _protected_ivars
-
-
variables.each_with_object({}) do |name, hash|
-
hash[name.slice(1, name.length)] = instance_variable_get(name)
-
end
-
end
-
-
1
private
-
# Normalize args by converting <tt>render "foo"</tt> to
-
# <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to
-
# <tt>render :file => "foo/bar"</tt>.
-
1
def _normalize_args(action = nil, options = {}) # :doc:
-
if action.respond_to?(:permitted?)
-
if action.permitted?
-
action
-
else
-
raise ArgumentError, "render parameters are not permitted"
-
end
-
elsif action.is_a?(Hash)
-
action
-
else
-
options
-
end
-
end
-
-
# Normalize options.
-
1
def _normalize_options(options) # :doc:
-
options
-
end
-
-
# Process extra options.
-
1
def _process_options(options) # :doc:
-
options
-
end
-
-
# Process the rendered format.
-
1
def _process_format(format) # :nodoc:
-
end
-
-
1
def _process_variant(options)
-
end
-
-
1
def _set_html_content_type # :nodoc:
-
end
-
-
1
def _set_vary_header # :nodoc:
-
end
-
-
1
def _set_rendered_content_type(format) # :nodoc:
-
end
-
-
# Normalize args and options.
-
1
def _normalize_render(*args, &block) # :nodoc:
-
options = _normalize_args(*args, &block)
-
_process_variant(options)
-
_normalize_options(options)
-
options
-
end
-
-
1
def _protected_ivars
-
DEFAULT_PROTECTED_INSTANCE_VARIABLES
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/symbol/starts_ends_with"
-
-
1
module AbstractController
-
1
module Translation
-
1
mattr_accessor :raise_on_missing_translations, default: false
-
-
# Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>.
-
#
-
# When the given key starts with a period, it will be scoped by the current
-
# controller and action. So if you call <tt>translate(".foo")</tt> from
-
# <tt>PeopleController#index</tt>, it will convert the call to
-
# <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
-
# to translate many keys within the same controller / action and gives you a
-
# simple framework for scoping them consistently.
-
1
def translate(key, **options)
-
if key&.start_with?(".")
-
path = controller_path.tr("/", ".")
-
defaults = [:"#{path}#{key}"]
-
defaults << options[:default] if options[:default]
-
options[:default] = defaults.flatten
-
key = "#{path}.#{action_name}#{key}"
-
end
-
-
i18n_raise = options.fetch(:raise, self.raise_on_missing_translations)
-
I18n.translate(key, **options, raise: i18n_raise)
-
end
-
1
alias :t :translate
-
-
# Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
-
1
def localize(object, **options)
-
I18n.localize(object, **options)
-
end
-
1
alias :l :localize
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module AbstractController
-
# Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class
-
# has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an
-
# exception will be raised.
-
#
-
# Note that this module is completely decoupled from HTTP - the only requirement is a valid
-
# <tt>_routes</tt> implementation.
-
1
module UrlFor
-
1
extend ActiveSupport::Concern
-
1
include ActionDispatch::Routing::UrlFor
-
-
1
def _routes
-
raise "In order to use #url_for, you must include routing helpers explicitly. " \
-
"For instance, `include Rails.application.routes.url_helpers`."
-
end
-
-
1
module ClassMethods
-
1
def _routes
-
nil
-
end
-
-
1
def action_methods
-
@action_methods ||= begin
-
if _routes
-
super - _routes.named_routes.helper_names
-
else
-
super
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_view"
-
1
require "action_controller/log_subscriber"
-
1
require "action_controller/metal/params_wrapper"
-
-
1
module ActionController
-
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
-
# on request and then either it renders a template or redirects to another action. An action is defined as a public method
-
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
-
#
-
# By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
-
# controllers inherit from ApplicationController. This gives you one class to configure things such as
-
# request forgery protection and filtering of sensitive request parameters.
-
#
-
# A sample controller could look like this:
-
#
-
# class PostsController < ApplicationController
-
# def index
-
# @posts = Post.all
-
# end
-
#
-
# def create
-
# @post = Post.create params[:post]
-
# redirect_to posts_path
-
# end
-
# end
-
#
-
# Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action
-
# after executing code in the action. For example, the +index+ action of the PostsController would render the
-
# template <tt>app/views/posts/index.html.erb</tt> by default after populating the <tt>@posts</tt> instance variable.
-
#
-
# Unlike index, the create action will not render a template. After performing its main purpose (creating a
-
# new post), it initiates a redirect instead. This redirect works by returning an external
-
# <tt>302 Moved</tt> HTTP response that takes the user to the index action.
-
#
-
# These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect.
-
# Most actions are variations on these themes.
-
#
-
# == Requests
-
#
-
# For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller
-
# and action are called. The remaining request parameters, the session (if one is available), and the full request with
-
# all the HTTP headers are made available to the action through accessor methods. Then the action is performed.
-
#
-
# The full request object is available via the request accessor and is primarily used to query for HTTP headers:
-
#
-
# def server_ip
-
# location = request.env["REMOTE_ADDR"]
-
# render plain: "This server hosted at #{location}"
-
# end
-
#
-
# == Parameters
-
#
-
# All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are
-
# available through the <tt>params</tt> method which returns a hash. For example, an action that was performed through
-
# <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in <tt>params</tt>.
-
#
-
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
-
#
-
# <input type="text" name="post[name]" value="david">
-
# <input type="text" name="post[address]" value="hyacintvej">
-
#
-
# A request coming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
-
# If the address input had been named <tt>post[address][street]</tt>, the <tt>params</tt> would have included
-
# <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
-
#
-
# == Sessions
-
#
-
# Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
-
# such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
-
# as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
-
# they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
-
#
-
# You can place objects in the session by using the <tt>session</tt> method, which accesses a hash:
-
#
-
# session[:person] = Person.authenticate(user_name, password)
-
#
-
# You can retrieve it again through the same hash:
-
#
-
# "Hello #{session[:person]}"
-
#
-
# For removing objects from the session, you can either assign a single key to +nil+:
-
#
-
# # removes :person from session
-
# session[:person] = nil
-
#
-
# or you can remove the entire session with +reset_session+.
-
#
-
# Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
-
# This prevents the user from tampering with the session but also allows them to see its contents.
-
#
-
# Do not put secret information in cookie-based sessions!
-
#
-
# == Responses
-
#
-
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
-
# object is generated automatically through the use of renders and redirects and requires no user intervention.
-
#
-
# == Renders
-
#
-
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
-
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERB templates. It's automatically configured.
-
# The controller passes objects to the view by assigning instance variables:
-
#
-
# def show
-
# @post = Post.find(params[:id])
-
# end
-
#
-
# Which are then automatically available to the view:
-
#
-
# Title: <%= @post.title %>
-
#
-
# You don't have to rely on the automated rendering. For example, actions that could result in the rendering of different templates
-
# will use the manual rendering methods:
-
#
-
# def search
-
# @results = Search.find(params[:query])
-
# case @results.count
-
# when 0 then render action: "no_results"
-
# when 1 then render action: "show"
-
# when 2..10 then render action: "show_many"
-
# end
-
# end
-
#
-
# Read more about writing ERB and Builder templates in ActionView::Base.
-
#
-
# == Redirects
-
#
-
# Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to the
-
# database, we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're
-
# going to reuse (and redirect to) a <tt>show</tt> action that we'll assume has already been created. The code might look like this:
-
#
-
# def create
-
# @entry = Entry.new(params[:entry])
-
# if @entry.save
-
# # The entry was saved correctly, redirect to show
-
# redirect_to action: 'show', id: @entry.id
-
# else
-
# # things didn't go so well, do something else
-
# end
-
# end
-
#
-
# In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method, which is then executed.
-
# Note that this is an external HTTP-level redirection which will cause the browser to make a second request (a GET to the show action),
-
# and not some internal re-routing which calls both "create" and then "show" within one request.
-
#
-
# Learn more about <tt>redirect_to</tt> and what options you have in ActionController::Redirecting.
-
#
-
# == Calling multiple redirects or renders
-
#
-
# An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError:
-
#
-
# def do_something
-
# redirect_to action: "elsewhere"
-
# render action: "overthere" # raises DoubleRenderError
-
# end
-
#
-
# If you need to redirect on the condition of something, then be sure to add "and return" to halt execution.
-
#
-
# def do_something
-
# redirect_to(action: "elsewhere") and return if monkeys.nil?
-
# render action: "overthere" # won't be called if monkeys is nil
-
# end
-
#
-
1
class Base < Metal
-
1
abstract!
-
-
# We document the request and response methods here because albeit they are
-
# implemented in ActionController::Metal, the type of the returned objects
-
# is unknown at that level.
-
-
##
-
# :method: request
-
#
-
# Returns an ActionDispatch::Request instance that represents the
-
# current request.
-
-
##
-
# :method: response
-
#
-
# Returns an ActionDispatch::Response that represents the current
-
# response.
-
-
# Shortcut helper that returns all the modules included in
-
# ActionController::Base except the ones passed as arguments:
-
#
-
# class MyBaseController < ActionController::Metal
-
# ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
-
# include left
-
# end
-
# end
-
#
-
# This gives better control over what you want to exclude and makes it
-
# easier to create a bare controller class, instead of listing the modules
-
# required manually.
-
1
def self.without_modules(*modules)
-
modules = modules.map do |m|
-
m.is_a?(Symbol) ? ActionController.const_get(m) : m
-
end
-
-
MODULES - modules
-
end
-
-
MODULES = [
-
1
AbstractController::Rendering,
-
AbstractController::Translation,
-
AbstractController::AssetPaths,
-
-
Helpers,
-
UrlFor,
-
Redirecting,
-
ActionView::Layouts,
-
Rendering,
-
Renderers::All,
-
ConditionalGet,
-
EtagWithTemplateDigest,
-
EtagWithFlash,
-
Caching,
-
MimeResponds,
-
ImplicitRender,
-
StrongParameters,
-
ParameterEncoding,
-
Cookies,
-
Flash,
-
FormBuilder,
-
RequestForgeryProtection,
-
ContentSecurityPolicy,
-
PermissionsPolicy,
-
Streaming,
-
DataStreaming,
-
HttpAuthentication::Basic::ControllerMethods,
-
HttpAuthentication::Digest::ControllerMethods,
-
HttpAuthentication::Token::ControllerMethods,
-
DefaultHeaders,
-
Logging,
-
-
# Before callbacks should also be executed as early as possible, so
-
# also include them at the bottom.
-
AbstractController::Callbacks,
-
-
# Append rescue at the bottom to wrap as much as possible.
-
Rescue,
-
-
# Add instrumentations hooks at the bottom, to ensure they instrument
-
# all the methods properly.
-
Instrumentation,
-
-
# Params wrapper should come before instrumentation so they are
-
# properly showed in logs
-
ParamsWrapper
-
]
-
-
1
MODULES.each do |mod|
-
34
include mod
-
end
-
1
setup_renderer!
-
-
# Define some internal variables that should not be propagated to the view.
-
1
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i(
-
@_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class
-
@_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy
-
)
-
-
1
def _protected_ivars
-
PROTECTED_IVARS
-
end
-
1
private :_protected_ivars
-
-
1
ActiveSupport.run_load_hooks(:action_controller_base, self)
-
1
ActiveSupport.run_load_hooks(:action_controller, self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# \Caching is a cheap way of speeding up slow applications by keeping the result of
-
# calculations, renderings, and database calls around for subsequent requests.
-
#
-
# You can read more about each approach by clicking the modules below.
-
#
-
# Note: To turn off all caching provided by Action Controller, set
-
# config.action_controller.perform_caching = false
-
#
-
# == \Caching stores
-
#
-
# All the caching stores from ActiveSupport::Cache are available to be used as backends
-
# for Action Controller caching.
-
#
-
# Configuration examples (FileStore is the default):
-
#
-
# config.action_controller.cache_store = :memory_store
-
# config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
-
# config.action_controller.cache_store = :mem_cache_store, 'localhost'
-
# config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211')
-
# config.action_controller.cache_store = MyOwnStore.new('parameter')
-
1
module Caching
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
include AbstractController::Caching
-
end
-
-
1
private
-
1
def instrument_payload(key)
-
{
-
controller: controller_name,
-
action: action_name,
-
key: key
-
}
-
end
-
-
1
def instrument_name
-
"action_controller"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# Override the default form builder for all views rendered by this
-
# controller and any of its descendants. Accepts a subclass of
-
# +ActionView::Helpers::FormBuilder+.
-
#
-
# For example, given a form builder:
-
#
-
# class AdminFormBuilder < ActionView::Helpers::FormBuilder
-
# def special_field(name)
-
# end
-
# end
-
#
-
# The controller specifies a form builder as its default:
-
#
-
# class AdminAreaController < ApplicationController
-
# default_form_builder AdminFormBuilder
-
# end
-
#
-
# Then in the view any form using +form_for+ will be an instance of the
-
# specified form builder:
-
#
-
# <%= form_for(@instance) do |builder| %>
-
# <%= builder.special_field(:name) %>
-
# <% end %>
-
1
module FormBuilder
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
class_attribute :_default_form_builder, instance_accessor: false
-
end
-
-
1
module ClassMethods
-
# Set the form builder to be used as the default for all forms
-
# in the views rendered by this controller and its subclasses.
-
#
-
# ==== Parameters
-
# * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+
-
1
def default_form_builder(builder)
-
self._default_form_builder = builder
-
end
-
end
-
-
# Default form builder for the controller
-
1
def default_form_builder
-
self.class._default_form_builder
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
class LogSubscriber < ActiveSupport::LogSubscriber
-
1
INTERNAL_PARAMS = %w(controller action format _method only_path)
-
-
1
def start_processing(event)
-
return unless logger.info?
-
-
payload = event.payload
-
params = payload[:params].except(*INTERNAL_PARAMS)
-
format = payload[:format]
-
format = format.to_s.upcase if format.is_a?(Symbol)
-
format = "*/*" if format.nil?
-
-
info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
-
info " Parameters: #{params.inspect}" unless params.empty?
-
end
-
-
1
def process_action(event)
-
info do
-
payload = event.payload
-
additions = ActionController::Base.log_process_action(payload)
-
status = payload[:status]
-
-
if status.nil? && (exception_class_name = payload[:exception]&.first)
-
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
-
end
-
-
additions << "Allocations: #{event.allocations}"
-
-
message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
-
message << " (#{additions.join(" | ")})"
-
message << "\n\n" if defined?(Rails.env) && Rails.env.development?
-
-
message
-
end
-
end
-
-
1
def halted_callback(event)
-
info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
-
end
-
-
1
def send_file(event)
-
info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
-
end
-
-
1
def redirect_to(event)
-
info { "Redirected to #{event.payload[:location]}" }
-
end
-
-
1
def send_data(event)
-
info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
-
end
-
-
1
def unpermitted_parameters(event)
-
debug do
-
unpermitted_keys = event.payload[:keys]
-
color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED)
-
end
-
end
-
-
1
%w(write_fragment read_fragment exist_fragment?
-
expire_fragment expire_page write_page).each do |method|
-
6
class_eval <<-METHOD, __FILE__, __LINE__ + 1
-
def #{method}(event)
-
return unless logger.info? && ActionController::Base.enable_fragment_cache_logging
-
key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
-
human_name = #{method.to_s.humanize.inspect}
-
info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
-
end
-
METHOD
-
end
-
-
1
def logger
-
ActionController::Base.logger
-
end
-
end
-
end
-
-
1
ActionController::LogSubscriber.attach_to :action_controller
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/array/extract_options"
-
1
require "action_dispatch/middleware/stack"
-
1
require "action_dispatch/http/request"
-
1
require "action_dispatch/http/response"
-
-
1
module ActionController
-
# Extend ActionDispatch middleware stack to make it aware of options
-
# allowing the following syntax in controllers:
-
#
-
# class PostsController < ApplicationController
-
# use AuthenticationMiddleware, except: [:index, :show]
-
# end
-
#
-
1
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
-
1
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
-
1
def initialize(klass, args, actions, strategy, block)
-
@actions = actions
-
@strategy = strategy
-
super(klass, args, block)
-
end
-
-
1
def valid?(action)
-
@strategy.call @actions, action
-
end
-
end
-
-
1
def build(action, app = nil, &block)
-
action = action.to_s
-
-
middlewares.reverse.inject(app || block) do |a, middleware|
-
middleware.valid?(action) ? middleware.build(a) : a
-
end
-
end
-
-
1
private
-
1
INCLUDE = ->(list, action) { list.include? action }
-
1
EXCLUDE = ->(list, action) { !list.include? action }
-
1
NULL = ->(list, action) { true }
-
-
1
def build_middleware(klass, args, block)
-
options = args.extract_options!
-
only = Array(options.delete(:only)).map(&:to_s)
-
except = Array(options.delete(:except)).map(&:to_s)
-
args << options unless options.empty?
-
-
strategy = NULL
-
list = nil
-
-
if only.any?
-
strategy = INCLUDE
-
list = only
-
elsif except.any?
-
strategy = EXCLUDE
-
list = except
-
end
-
-
Middleware.new(klass, args, list, strategy, block)
-
end
-
end
-
-
# <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
-
# valid Rack interface without the additional niceties provided by
-
# <tt>ActionController::Base</tt>.
-
#
-
# A sample metal controller might look like this:
-
#
-
# class HelloController < ActionController::Metal
-
# def index
-
# self.response_body = "Hello World!"
-
# end
-
# end
-
#
-
# And then to route requests to your metal controller, you would add
-
# something like this to <tt>config/routes.rb</tt>:
-
#
-
# get 'hello', to: HelloController.action(:index)
-
#
-
# The +action+ method returns a valid Rack application for the \Rails
-
# router to dispatch to.
-
#
-
# == Rendering Helpers
-
#
-
# <tt>ActionController::Metal</tt> by default provides no utilities for rendering
-
# views, partials, or other responses aside from explicitly calling of
-
# <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To
-
# add the render helpers you're used to having in a normal controller, you
-
# can do the following:
-
#
-
# class HelloController < ActionController::Metal
-
# include AbstractController::Rendering
-
# include ActionView::Layouts
-
# append_view_path "#{Rails.root}/app/views"
-
#
-
# def index
-
# render "hello/index"
-
# end
-
# end
-
#
-
# == Redirection Helpers
-
#
-
# To add redirection helpers to your metal controller, do the following:
-
#
-
# class HelloController < ActionController::Metal
-
# include ActionController::Redirecting
-
# include Rails.application.routes.url_helpers
-
#
-
# def index
-
# redirect_to root_url
-
# end
-
# end
-
#
-
# == Other Helpers
-
#
-
# You can refer to the modules included in <tt>ActionController::Base</tt> to see
-
# other features you can bring into your metal controller.
-
#
-
1
class Metal < AbstractController::Base
-
1
abstract!
-
-
# Returns the last part of the controller's name, underscored, without the ending
-
# <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
-
# Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
-
#
-
# ==== Returns
-
# * <tt>string</tt>
-
1
def self.controller_name
-
@controller_name ||= (name.demodulize.delete_suffix("Controller").underscore unless anonymous?)
-
end
-
-
1
def self.make_response!(request)
-
ActionDispatch::Response.new.tap do |res|
-
res.request = request
-
end
-
end
-
-
1
def self.action_encoding_template(action) # :nodoc:
-
false
-
end
-
-
# Delegates to the class' <tt>controller_name</tt>.
-
1
def controller_name
-
self.class.controller_name
-
end
-
-
1
attr_internal :response, :request
-
1
delegate :session, to: "@_request"
-
1
delegate :headers, :status=, :location=, :content_type=,
-
:status, :location, :content_type, :media_type, to: "@_response"
-
-
1
def initialize
-
@_request = nil
-
@_response = nil
-
@_routes = nil
-
super
-
end
-
-
1
def params
-
@_params ||= request.parameters
-
end
-
-
1
def params=(val)
-
@_params = val
-
end
-
-
1
alias :response_code :status # :nodoc:
-
-
# Basic url_for that can be overridden for more robust functionality.
-
1
def url_for(string)
-
string
-
end
-
-
1
def response_body=(body)
-
body = [body] unless body.nil? || body.respond_to?(:each)
-
response.reset_body!
-
return unless body
-
response.body = body
-
super
-
end
-
-
# Tests if render or redirect has already happened.
-
1
def performed?
-
response_body || response.committed?
-
end
-
-
1
def dispatch(name, request, response) #:nodoc:
-
set_request!(request)
-
set_response!(response)
-
process(name)
-
request.commit_flash
-
to_a
-
end
-
-
1
def set_response!(response) # :nodoc:
-
@_response = response
-
end
-
-
1
def set_request!(request) #:nodoc:
-
@_request = request
-
@_request.controller_instance = self
-
end
-
-
1
def to_a #:nodoc:
-
response.to_a
-
end
-
-
1
def reset_session
-
@_request.reset_session
-
end
-
-
1
class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
-
-
1
def self.inherited(base) # :nodoc:
-
2
base.middleware_stack = middleware_stack.dup
-
2
super
-
end
-
-
1
class << self
-
# Pushes the given Rack middleware and its arguments to the bottom of the
-
# middleware stack.
-
1
def use(*args, &block)
-
middleware_stack.use(*args, &block)
-
end
-
1
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
-
end
-
-
# Alias for +middleware_stack+.
-
1
def self.middleware
-
middleware_stack
-
end
-
-
# Returns a Rack endpoint for the given action name.
-
1
def self.action(name)
-
app = lambda { |env|
-
req = ActionDispatch::Request.new(env)
-
res = make_response! req
-
new.dispatch(name, req, res)
-
}
-
-
if middleware_stack.any?
-
middleware_stack.build(name, app)
-
else
-
app
-
end
-
end
-
-
# Direct dispatch to the controller. Instantiates the controller, then
-
# executes the action named +name+.
-
1
def self.dispatch(name, req, res)
-
if middleware_stack.any?
-
middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
-
else
-
new.dispatch(name, req, res)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module BasicImplicitRender # :nodoc:
-
1
def send_action(method, *args)
-
super.tap { default_render unless performed? }
-
end
-
-
1
def default_render
-
head :no_content
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/object/try"
-
1
require "active_support/core_ext/integer/time"
-
-
1
module ActionController
-
1
module ConditionalGet
-
1
extend ActiveSupport::Concern
-
-
1
include Head
-
-
1
included do
-
1
class_attribute :etaggers, default: []
-
end
-
-
1
module ClassMethods
-
# Allows you to consider additional controller-wide information when generating an ETag.
-
# For example, if you serve pages tailored depending on who's logged in at the moment, you
-
# may want to add the current user id to be part of the ETag to prevent unauthorized displaying
-
# of cached pages.
-
#
-
# class InvoicesController < ApplicationController
-
# etag { current_user&.id }
-
#
-
# def show
-
# # Etag will differ even for the same invoice when it's viewed by a different current_user
-
# @invoice = Invoice.find(params[:id])
-
# fresh_when etag: @invoice
-
# end
-
# end
-
1
def etag(&etagger)
-
2
self.etaggers += [etagger]
-
end
-
end
-
-
# Sets the +etag+, +last_modified+, or both on the response and renders a
-
# <tt>304 Not Modified</tt> response if the request is already fresh.
-
#
-
# === Parameters:
-
#
-
# * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
-
# +:weak_etag+ option.
-
# * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
-
# Requests that set If-None-Match header may return a 304 Not Modified
-
# response if it matches the ETag exactly. A weak ETag indicates semantic
-
# equivalence, not byte-for-byte equality, so they're good for caching
-
# HTML pages in browser caches. They can't be used for responses that
-
# must be byte-identical, like serving Range requests within a PDF file.
-
# * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
-
# Requests that set If-None-Match header may return a 304 Not Modified
-
# response if it matches the ETag exactly. A strong ETag implies exact
-
# equality: the response must match byte for byte. This is necessary for
-
# doing Range requests within a large video or PDF file, for example, or
-
# for compatibility with some CDNs that don't support weak ETags.
-
# * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
-
# response. Subsequent requests that set If-Modified-Since may return a
-
# 304 Not Modified response if last_modified <= If-Modified-Since.
-
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
-
# +true+ if you want your application to be cacheable by other devices (proxy caches).
-
# * <tt>:template</tt> By default, the template digest for the current
-
# controller/action is included in ETags. If the action renders a
-
# different template, you can include its digest instead. If the action
-
# doesn't render a template at all, you can pass <tt>template: false</tt>
-
# to skip any attempt to check for a template digest.
-
#
-
# === Example:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
# fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
-
# end
-
#
-
# This will render the show template if the request isn't sending a matching ETag or
-
# If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
-
#
-
# You can also just pass a record. In this case +last_modified+ will be set
-
# by calling +updated_at+ and +etag+ by passing the object itself.
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
# fresh_when(@article)
-
# end
-
#
-
# You can also pass an object that responds to +maximum+, such as a
-
# collection of active records. In this case +last_modified+ will be set by
-
# calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
-
# most recently updated record) and the +etag+ by passing the object itself.
-
#
-
# def index
-
# @articles = Article.all
-
# fresh_when(@articles)
-
# end
-
#
-
# When passing a record or a collection, you can still set the public header:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
# fresh_when(@article, public: true)
-
# end
-
#
-
# When rendering a different template than the default controller/action
-
# style, you can indicate which digest to include in the ETag:
-
#
-
# before_action { fresh_when @article, template: 'widgets/show' }
-
#
-
1
def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
-
weak_etag ||= etag || object unless strong_etag
-
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
-
-
if strong_etag
-
response.strong_etag = combine_etags strong_etag,
-
last_modified: last_modified, public: public, template: template
-
elsif weak_etag || template
-
response.weak_etag = combine_etags weak_etag,
-
last_modified: last_modified, public: public, template: template
-
end
-
-
response.last_modified = last_modified if last_modified
-
response.cache_control[:public] = true if public
-
-
head :not_modified if request.fresh?(response)
-
end
-
-
# Sets the +etag+ and/or +last_modified+ on the response and checks it against
-
# the client request. If the request doesn't match the options provided, the
-
# request is considered stale and should be generated from scratch. Otherwise,
-
# it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
-
#
-
# === Parameters:
-
#
-
# * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
-
# +:weak_etag+ option.
-
# * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
-
# Requests that set If-None-Match header may return a 304 Not Modified
-
# response if it matches the ETag exactly. A weak ETag indicates semantic
-
# equivalence, not byte-for-byte equality, so they're good for caching
-
# HTML pages in browser caches. They can't be used for responses that
-
# must be byte-identical, like serving Range requests within a PDF file.
-
# * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
-
# Requests that set If-None-Match header may return a 304 Not Modified
-
# response if it matches the ETag exactly. A strong ETag implies exact
-
# equality: the response must match byte for byte. This is necessary for
-
# doing Range requests within a large video or PDF file, for example, or
-
# for compatibility with some CDNs that don't support weak ETags.
-
# * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
-
# response. Subsequent requests that set If-Modified-Since may return a
-
# 304 Not Modified response if last_modified <= If-Modified-Since.
-
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
-
# +true+ if you want your application to be cacheable by other devices (proxy caches).
-
# * <tt>:template</tt> By default, the template digest for the current
-
# controller/action is included in ETags. If the action renders a
-
# different template, you can include its digest instead. If the action
-
# doesn't render a template at all, you can pass <tt>template: false</tt>
-
# to skip any attempt to check for a template digest.
-
#
-
# === Example:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
#
-
# if stale?(etag: @article, last_modified: @article.updated_at)
-
# @statistics = @article.really_expensive_call
-
# respond_to do |format|
-
# # all the supported formats
-
# end
-
# end
-
# end
-
#
-
# You can also just pass a record. In this case +last_modified+ will be set
-
# by calling +updated_at+ and +etag+ by passing the object itself.
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
#
-
# if stale?(@article)
-
# @statistics = @article.really_expensive_call
-
# respond_to do |format|
-
# # all the supported formats
-
# end
-
# end
-
# end
-
#
-
# You can also pass an object that responds to +maximum+, such as a
-
# collection of active records. In this case +last_modified+ will be set by
-
# calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
-
# most recently updated record) and the +etag+ by passing the object itself.
-
#
-
# def index
-
# @articles = Article.all
-
#
-
# if stale?(@articles)
-
# @statistics = @articles.really_expensive_call
-
# respond_to do |format|
-
# # all the supported formats
-
# end
-
# end
-
# end
-
#
-
# When passing a record or a collection, you can still set the public header:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
#
-
# if stale?(@article, public: true)
-
# @statistics = @article.really_expensive_call
-
# respond_to do |format|
-
# # all the supported formats
-
# end
-
# end
-
# end
-
#
-
# When rendering a different template than the default controller/action
-
# style, you can indicate which digest to include in the ETag:
-
#
-
# def show
-
# super if stale? @article, template: 'widgets/show'
-
# end
-
#
-
1
def stale?(object = nil, **freshness_kwargs)
-
fresh_when(object, **freshness_kwargs)
-
!request.fresh?(response)
-
end
-
-
# Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
-
# instruction, so that intermediate caches must not cache the response.
-
#
-
# expires_in 20.minutes
-
# expires_in 3.hours, public: true
-
# expires_in 3.hours, public: true, must_revalidate: true
-
#
-
# This method will overwrite an existing Cache-Control header.
-
# See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
-
#
-
# HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861
-
# It helps to cache an asset and serve it while is being revalidated and/or returning with an error.
-
#
-
# expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds
-
# expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes
-
#
-
# HTTP Cache-Control Extensions other values: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
-
# Any additional key-value pairs are concatenated onto the `Cache-Control` header in the response:
-
#
-
# expires_in 3.hours, public: true, "s-maxage": 3.hours, "no-transform": true
-
#
-
# The method will also ensure an HTTP Date header for client compatibility.
-
1
def expires_in(seconds, options = {})
-
response.cache_control.merge!(
-
max_age: seconds,
-
public: options.delete(:public),
-
must_revalidate: options.delete(:must_revalidate),
-
stale_while_revalidate: options.delete(:stale_while_revalidate),
-
stale_if_error: options.delete(:stale_if_error),
-
)
-
options.delete(:private)
-
-
response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
-
response.date = Time.now unless response.date?
-
end
-
-
# Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
-
# resource will be marked as stale, so clients must always revalidate.
-
# Intermediate/browser caches may still store the asset.
-
1
def expires_now
-
response.cache_control.replace(no_cache: true)
-
end
-
-
# Cache or yield the block. The cache is supposed to never expire.
-
#
-
# You can use this method when you have an HTTP response that never changes,
-
# and the browser and proxies should cache it indefinitely.
-
#
-
# * +public+: By default, HTTP responses are private, cached only on the
-
# user's web browser. To allow proxies to cache the response, set +true+ to
-
# indicate that they can serve the cached response to all users.
-
1
def http_cache_forever(public: false)
-
expires_in 100.years, public: public
-
-
yield if stale?(etag: request.fullpath,
-
last_modified: Time.new(2011, 1, 1).utc,
-
public: public)
-
end
-
-
1
private
-
1
def combine_etags(validator, options)
-
[validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController #:nodoc:
-
1
module ContentSecurityPolicy
-
# TODO: Documentation
-
1
extend ActiveSupport::Concern
-
-
1
include AbstractController::Helpers
-
1
include AbstractController::Callbacks
-
-
1
included do
-
1
helper_method :content_security_policy?
-
1
helper_method :content_security_policy_nonce
-
end
-
-
1
module ClassMethods
-
1
def content_security_policy(enabled = true, **options, &block)
-
before_action(options) do
-
if block_given?
-
policy = current_content_security_policy
-
yield policy
-
request.content_security_policy = policy
-
end
-
-
unless enabled
-
request.content_security_policy = nil
-
end
-
end
-
end
-
-
1
def content_security_policy_report_only(report_only = true, **options)
-
before_action(options) do
-
request.content_security_policy_report_only = report_only
-
end
-
end
-
end
-
-
1
private
-
1
def content_security_policy?
-
request.content_security_policy
-
end
-
-
1
def content_security_policy_nonce
-
request.content_security_policy_nonce
-
end
-
-
1
def current_content_security_policy
-
request.content_security_policy&.clone || ActionDispatch::ContentSecurityPolicy.new
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController #:nodoc:
-
1
module Cookies
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
helper_method :cookies if defined?(helper_method)
-
end
-
-
1
private
-
# The cookies for the current request. See ActionDispatch::Cookies for
-
# more information.
-
1
def cookies # :doc:
-
request.cookie_jar
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_controller/metal/exceptions"
-
1
require "action_dispatch/http/content_disposition"
-
-
1
module ActionController #:nodoc:
-
# Methods for sending arbitrary data and for streaming files to the browser,
-
# instead of rendering.
-
1
module DataStreaming
-
1
extend ActiveSupport::Concern
-
-
1
include ActionController::Rendering
-
-
1
DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc:
-
1
DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc:
-
-
1
private
-
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
-
# via the Rack::Sendfile middleware. The header to use is set via
-
# +config.action_dispatch.x_sendfile_header+.
-
# Your server can also configure this for you by setting the X-Sendfile-Type header.
-
#
-
# Be careful to sanitize the path parameter if it is coming from a web
-
# page. <tt>send_file(params[:path])</tt> allows a malicious user to
-
# download any file on your server.
-
#
-
# Options:
-
# * <tt>:filename</tt> - suggests a filename for the browser to use.
-
# Defaults to <tt>File.basename(path)</tt>.
-
# * <tt>:type</tt> - specifies an HTTP content type.
-
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
-
# If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>.
-
# If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
-
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
-
# Valid values are 'inline' and 'attachment' (default).
-
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
-
# * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from
-
# the URL, which is necessary for i18n filenames on certain browsers
-
# (setting <tt>:filename</tt> overrides this option).
-
#
-
# The default Content-Type and Content-Disposition headers are
-
# set to download arbitrary binary files in as many browsers as
-
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
-
# a variety of quirks (especially when downloading over SSL).
-
#
-
# Simple download:
-
#
-
# send_file '/path/to.zip'
-
#
-
# Show a JPEG in the browser:
-
#
-
# send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline'
-
#
-
# Show a 404 page in the browser:
-
#
-
# send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404
-
#
-
# Read about the other Content-* HTTP headers if you'd like to
-
# provide the user with more information (such as Content-Description) in
-
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
-
#
-
# Also be aware that the document may be cached by proxies and browsers.
-
# The Pragma and Cache-Control headers declare how the file may be cached
-
# by intermediaries. They default to require clients to validate with
-
# the server before releasing cached responses. See
-
# https://www.mnot.net/cache_docs/ for an overview of web caching and
-
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
-
# for the Cache-Control header spec.
-
1
def send_file(path, options = {}) #:doc:
-
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
-
-
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
-
send_file_headers! options
-
-
self.status = options[:status] || 200
-
self.content_type = options[:content_type] if options.key?(:content_type)
-
response.send_file path
-
end
-
-
# Sends the given binary data to the browser. This method is similar to
-
# <tt>render plain: data</tt>, but also allows you to specify whether
-
# the browser should display the response as a file attachment (i.e. in a
-
# download dialog) or as inline data. You may also set the content type,
-
# the file name, and other things.
-
#
-
# Options:
-
# * <tt>:filename</tt> - suggests a filename for the browser to use.
-
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
-
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
-
# If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
-
# If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
-
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
-
# Valid values are 'inline' and 'attachment' (default).
-
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
-
#
-
# Generic data download:
-
#
-
# send_data buffer
-
#
-
# Download a dynamically-generated tarball:
-
#
-
# send_data generate_tgz('dir'), filename: 'dir.tgz'
-
#
-
# Display an image Active Record in the browser:
-
#
-
# send_data image.data, type: image.content_type, disposition: 'inline'
-
#
-
# See +send_file+ for more information on HTTP Content-* headers and caching.
-
1
def send_data(data, options = {}) #:doc:
-
send_file_headers! options
-
render options.slice(:status, :content_type).merge(body: data)
-
end
-
-
1
def send_file_headers!(options)
-
type_provided = options.has_key?(:type)
-
-
content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE)
-
self.content_type = content_type
-
response.sending_file = true
-
-
raise ArgumentError, ":type option required" if content_type.nil?
-
-
if content_type.is_a?(Symbol)
-
extension = Mime[content_type]
-
raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension
-
self.content_type = extension
-
else
-
if !type_provided && options[:filename]
-
# If type wasn't provided, try guessing from file extension.
-
content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete(".")) || content_type
-
end
-
self.content_type = content_type
-
end
-
-
disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
-
if disposition
-
headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename])
-
end
-
-
headers["Content-Transfer-Encoding"] = "binary"
-
-
# Fix a problem with IE 6.0 on opening downloaded files:
-
# If Cache-Control: no-cache is set (which Rails does by default),
-
# IE removes the file it just downloaded from its cache immediately
-
# after it displays the "open/save" dialog, which means that if you
-
# hit "open" the file isn't there anymore when the application that
-
# is called for handling the download is run, so let's workaround that
-
response.cache_control[:public] ||= false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# Allows configuring default headers that will be automatically merged into
-
# each response.
-
1
module DefaultHeaders
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
1
def make_response!(request)
-
ActionDispatch::Response.create.tap do |res|
-
res.request = request
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# When you're using the flash, it's generally used as a conditional on the view.
-
# This means the content of the view depends on the flash. Which in turn means
-
# that the ETag for a response should be computed with the content of the flash
-
# in mind. This does that by including the content of the flash as a component
-
# in the ETag that's generated for a response.
-
1
module EtagWithFlash
-
1
extend ActiveSupport::Concern
-
-
1
include ActionController::ConditionalGet
-
-
1
included do
-
1
etag { flash unless flash.empty? }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# When our views change, they should bubble up into HTTP cache freshness
-
# and bust browser caches. So the template digest for the current action
-
# is automatically included in the ETag.
-
#
-
# Enabled by default for apps that use Action View. Disable by setting
-
#
-
# config.action_controller.etag_with_template_digest = false
-
#
-
# Override the template to digest by passing +:template+ to +fresh_when+
-
# and +stale?+ calls. For example:
-
#
-
# # We're going to render widgets/show, not posts/show
-
# fresh_when @post, template: 'widgets/show'
-
#
-
# # We're not going to render a template, so omit it from the ETag.
-
# fresh_when @post, template: false
-
#
-
1
module EtagWithTemplateDigest
-
1
extend ActiveSupport::Concern
-
-
1
include ActionController::ConditionalGet
-
-
1
included do
-
1
class_attribute :etag_with_template_digest, default: true
-
-
1
etag do |options|
-
determine_template_etag(options) if etag_with_template_digest
-
end
-
end
-
-
1
private
-
1
def determine_template_etag(options)
-
if template = pick_template_for_etag(options)
-
lookup_and_digest_template(template)
-
end
-
end
-
-
# Pick the template digest to include in the ETag. If the +:template+ option
-
# is present, use the named template. If +:template+ is +nil+ or absent, use
-
# the default controller/action template. If +:template+ is false, omit the
-
# template digest from the ETag.
-
1
def pick_template_for_etag(options)
-
unless options[:template] == false
-
options[:template] || "#{controller_path}/#{action_name}"
-
end
-
end
-
-
1
def lookup_and_digest_template(template)
-
ActionView::Digestor.digest name: template, format: nil, finder: lookup_context
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController #:nodoc:
-
1
module Flash
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
class_attribute :_flash_types, instance_accessor: false, default: []
-
-
1
delegate :flash, to: :request
-
1
add_flash_types(:alert, :notice)
-
end
-
-
1
module ClassMethods
-
# Creates new flash types. You can pass as many types as you want to create
-
# flash types other than the default <tt>alert</tt> and <tt>notice</tt> in
-
# your controllers and views. For instance:
-
#
-
# # in application_controller.rb
-
# class ApplicationController < ActionController::Base
-
# add_flash_types :warning
-
# end
-
#
-
# # in your controller
-
# redirect_to user_path(@user), warning: "Incomplete profile"
-
#
-
# # in your view
-
# <%= warning %>
-
#
-
# This method will automatically define a new method for each of the given
-
# names, and it will be available in your views.
-
1
def add_flash_types(*types)
-
1
types.each do |type|
-
2
next if _flash_types.include?(type)
-
-
2
define_method(type) do
-
request.flash[type]
-
end
-
2
helper_method(type) if respond_to?(:helper_method)
-
-
2
self._flash_types += [type]
-
end
-
end
-
end
-
-
1
private
-
1
def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
-
self.class._flash_types.each do |flash_type|
-
if type = response_options_and_flash.delete(flash_type)
-
flash[flash_type] = type
-
end
-
end
-
-
if other_flashes = response_options_and_flash.delete(:flash)
-
flash.update(other_flashes)
-
end
-
-
super(options, response_options_and_flash)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module Head
-
# Returns a response that has no content (merely headers). The options
-
# argument is interpreted to be a hash of header names and values.
-
# This allows you to easily return a response that consists only of
-
# significant headers:
-
#
-
# head :created, location: person_path(@person)
-
#
-
# head :created, location: @person
-
#
-
# It can also be used to return exceptional conditions:
-
#
-
# return head(:method_not_allowed) unless request.post?
-
# return head(:bad_request) unless valid_request?
-
# render
-
#
-
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
-
1
def head(status, options = {})
-
if status.is_a?(Hash)
-
raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
-
end
-
-
status ||= :ok
-
-
location = options.delete(:location)
-
content_type = options.delete(:content_type)
-
-
options.each do |key, value|
-
headers[key.to_s.split(/[-_]/).each { |v| v[0] = v[0].upcase }.join("-")] = value.to_s
-
end
-
-
self.status = status
-
self.location = url_for(location) if location
-
-
if include_content?(response_code)
-
unless self.media_type
-
self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
-
end
-
-
response.charset = false
-
end
-
-
self.response_body = ""
-
-
true
-
end
-
-
1
private
-
1
def include_content?(status)
-
case status
-
when 100..199
-
false
-
when 204, 205, 304
-
false
-
else
-
true
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "base64"
-
1
require "active_support/security_utils"
-
-
1
module ActionController
-
# Makes it dead easy to do HTTP Basic, Digest and Token authentication.
-
1
module HttpAuthentication
-
# Makes it dead easy to do HTTP \Basic authentication.
-
#
-
# === Simple \Basic example
-
#
-
# class PostsController < ApplicationController
-
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index
-
#
-
# def index
-
# render plain: "Everyone can see me!"
-
# end
-
#
-
# def edit
-
# render plain: "I'm only accessible if you know the password"
-
# end
-
# end
-
#
-
# === Advanced \Basic example
-
#
-
# Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
-
# the regular HTML interface is protected by a session approach:
-
#
-
# class ApplicationController < ActionController::Base
-
# before_action :set_account, :authenticate
-
#
-
# private
-
# def set_account
-
# @account = Account.find_by(url_name: request.subdomains.first)
-
# end
-
#
-
# def authenticate
-
# case request.format
-
# when Mime[:xml], Mime[:atom]
-
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
-
# @current_user = user
-
# else
-
# request_http_basic_authentication
-
# end
-
# else
-
# if session_authenticated?
-
# @current_user = @account.users.find(session[:authenticated][:user_id])
-
# else
-
# redirect_to(login_url) and return false
-
# end
-
# end
-
# end
-
# end
-
#
-
# In your integration tests, you can do something like this:
-
#
-
# def test_access_granted_from_xml
-
# authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
-
#
-
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
-
#
-
# assert_equal 200, status
-
# end
-
1
module Basic
-
1
extend self
-
-
1
module ControllerMethods
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
1
def http_basic_authenticate_with(name:, password:, realm: nil, **options)
-
before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
-
end
-
end
-
-
1
def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
-
authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
-
# This comparison uses & so that it doesn't short circuit and
-
# uses `secure_compare` so that length information isn't leaked.
-
ActiveSupport::SecurityUtils.secure_compare(given_name, name) &
-
ActiveSupport::SecurityUtils.secure_compare(given_password, password)
-
end
-
end
-
-
1
def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
-
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
-
end
-
-
1
def authenticate_with_http_basic(&login_procedure)
-
HttpAuthentication::Basic.authenticate(request, &login_procedure)
-
end
-
-
1
def request_http_basic_authentication(realm = "Application", message = nil)
-
HttpAuthentication::Basic.authentication_request(self, realm, message)
-
end
-
end
-
-
1
def authenticate(request, &login_procedure)
-
if has_basic_credentials?(request)
-
login_procedure.call(*user_name_and_password(request))
-
end
-
end
-
-
1
def has_basic_credentials?(request)
-
request.authorization.present? && (auth_scheme(request).downcase == "basic")
-
end
-
-
1
def user_name_and_password(request)
-
decode_credentials(request).split(":", 2)
-
end
-
-
1
def decode_credentials(request)
-
::Base64.decode64(auth_param(request) || "")
-
end
-
-
1
def auth_scheme(request)
-
request.authorization.to_s.split(" ", 2).first
-
end
-
-
1
def auth_param(request)
-
request.authorization.to_s.split(" ", 2).second
-
end
-
-
1
def encode_credentials(user_name, password)
-
"Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
-
end
-
-
1
def authentication_request(controller, realm, message)
-
message ||= "HTTP Basic: Access denied.\n"
-
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
-
controller.status = 401
-
controller.response_body = message
-
end
-
end
-
-
# Makes it dead easy to do HTTP \Digest authentication.
-
#
-
# === Simple \Digest example
-
#
-
# require "digest/md5"
-
# class PostsController < ApplicationController
-
# REALM = "SuperSecret"
-
# USERS = {"dhh" => "secret", #plain text password
-
# "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
-
#
-
# before_action :authenticate, except: [:index]
-
#
-
# def index
-
# render plain: "Everyone can see me!"
-
# end
-
#
-
# def edit
-
# render plain: "I'm only accessible if you know the password"
-
# end
-
#
-
# private
-
# def authenticate
-
# authenticate_or_request_with_http_digest(REALM) do |username|
-
# USERS[username]
-
# end
-
# end
-
# end
-
#
-
# === Notes
-
#
-
# The +authenticate_or_request_with_http_digest+ block must return the user's password
-
# or the ha1 digest hash so the framework can appropriately hash to check the user's
-
# credentials. Returning +nil+ will cause authentication to fail.
-
#
-
# Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
-
# the password file or database is compromised, the attacker would be able to use the ha1 hash to
-
# authenticate as the user at this +realm+, but would not have the user's password to try using at
-
# other sites.
-
#
-
# In rare instances, web servers or front proxies strip authorization headers before
-
# they reach your application. You can debug this situation by logging all environment
-
# variables, and check for HTTP_AUTHORIZATION, amongst others.
-
1
module Digest
-
1
extend self
-
-
1
module ControllerMethods
-
1
def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
-
authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
-
end
-
-
# Authenticate with HTTP Digest, returns true or false
-
1
def authenticate_with_http_digest(realm = "Application", &password_procedure)
-
HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
-
end
-
-
# Render output including the HTTP Digest authentication header
-
1
def request_http_digest_authentication(realm = "Application", message = nil)
-
HttpAuthentication::Digest.authentication_request(self, realm, message)
-
end
-
end
-
-
# Returns false on a valid response, true otherwise
-
1
def authenticate(request, realm, &password_procedure)
-
request.authorization && validate_digest_response(request, realm, &password_procedure)
-
end
-
-
# Returns false unless the request credentials response value matches the expected value.
-
# First try the password as a ha1 digest password. If this fails, then try it as a plain
-
# text password.
-
1
def validate_digest_response(request, realm, &password_procedure)
-
secret_key = secret_token(request)
-
credentials = decode_credentials_header(request)
-
valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
-
-
if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
-
password = password_procedure.call(credentials[:username])
-
return false unless password
-
-
method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
-
uri = credentials[:uri]
-
-
[true, false].any? do |trailing_question_mark|
-
[true, false].any? do |password_is_ha1|
-
_uri = trailing_question_mark ? uri + "?" : uri
-
expected = expected_response(method, _uri, credentials, password, password_is_ha1)
-
expected == credentials[:response]
-
end
-
end
-
end
-
end
-
-
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
-
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
-
# of a plain-text password.
-
1
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
-
ha1 = password_is_ha1 ? password : ha1(credentials, password)
-
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
-
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
-
end
-
-
1
def ha1(credentials, password)
-
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
-
end
-
-
1
def encode_credentials(http_method, credentials, password, password_is_ha1)
-
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
-
"Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
-
end
-
-
1
def decode_credentials_header(request)
-
decode_credentials(request.authorization)
-
end
-
-
1
def decode_credentials(header)
-
ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
-
key, value = pair.split("=", 2)
-
[key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
-
end]
-
end
-
-
1
def authentication_header(controller, realm)
-
secret_key = secret_token(controller.request)
-
nonce = self.nonce(secret_key)
-
opaque = opaque(secret_key)
-
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
-
end
-
-
1
def authentication_request(controller, realm, message = nil)
-
message ||= "HTTP Digest: Access denied.\n"
-
authentication_header(controller, realm)
-
controller.status = 401
-
controller.response_body = message
-
end
-
-
1
def secret_token(request)
-
key_generator = request.key_generator
-
http_auth_salt = request.http_auth_salt
-
key_generator.generate_key(http_auth_salt)
-
end
-
-
# Uses an MD5 digest based on time to generate a value to be used only once.
-
#
-
# A server-specified data string which should be uniquely generated each time a 401 response is made.
-
# It is recommended that this string be base64 or hexadecimal data.
-
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
-
#
-
# The contents of the nonce are implementation dependent.
-
# The quality of the implementation depends on a good choice.
-
# A nonce might, for example, be constructed as the base 64 encoding of
-
#
-
# time-stamp H(time-stamp ":" ETag ":" private-key)
-
#
-
# where time-stamp is a server-generated time or other non-repeating value,
-
# ETag is the value of the HTTP ETag header associated with the requested entity,
-
# and private-key is data known only to the server.
-
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
-
# reject the request if it did not match the nonce from that header or
-
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
-
# The inclusion of the ETag prevents a replay request for an updated version of the resource.
-
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability
-
# to limit the reuse of the nonce to the same client that originally got it.
-
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
-
# Also, IP address spoofing is not that hard.)
-
#
-
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
-
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
-
# POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
-
# of this document.
-
#
-
# The nonce is opaque to the client. Composed of Time, and hash of Time with secret
-
# key from the Rails session secret generated upon creation of project. Ensures
-
# the time cannot be modified by client.
-
1
def nonce(secret_key, time = Time.now)
-
t = time.to_i
-
hashed = [t, secret_key]
-
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
-
::Base64.strict_encode64("#{t}:#{digest}")
-
end
-
-
# Might want a shorter timeout depending on whether the request
-
# is a PATCH, PUT, or POST, and if the client is a browser or web service.
-
# Can be much shorter if the Stale directive is implemented. This would
-
# allow a user to use new nonce without prompting the user again for their
-
# username and password.
-
1
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
-
return false if value.nil?
-
t = ::Base64.decode64(value).split(":").first.to_i
-
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
-
end
-
-
# Opaque based on digest of secret key
-
1
def opaque(secret_key)
-
::Digest::MD5.hexdigest(secret_key)
-
end
-
end
-
-
# Makes it dead easy to do HTTP Token authentication.
-
#
-
# Simple Token example:
-
#
-
# class PostsController < ApplicationController
-
# TOKEN = "secret"
-
#
-
# before_action :authenticate, except: [ :index ]
-
#
-
# def index
-
# render plain: "Everyone can see me!"
-
# end
-
#
-
# def edit
-
# render plain: "I'm only accessible if you know the password"
-
# end
-
#
-
# private
-
# def authenticate
-
# authenticate_or_request_with_http_token do |token, options|
-
# # Compare the tokens in a time-constant manner, to mitigate
-
# # timing attacks.
-
# ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
-
# end
-
# end
-
# end
-
#
-
#
-
# Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
-
# the regular HTML interface is protected by a session approach:
-
#
-
# class ApplicationController < ActionController::Base
-
# before_action :set_account, :authenticate
-
#
-
# private
-
# def set_account
-
# @account = Account.find_by(url_name: request.subdomains.first)
-
# end
-
#
-
# def authenticate
-
# case request.format
-
# when Mime[:xml], Mime[:atom]
-
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
-
# @current_user = user
-
# else
-
# request_http_token_authentication
-
# end
-
# else
-
# if session_authenticated?
-
# @current_user = @account.users.find(session[:authenticated][:user_id])
-
# else
-
# redirect_to(login_url) and return false
-
# end
-
# end
-
# end
-
# end
-
#
-
#
-
# In your integration tests, you can do something like this:
-
#
-
# def test_access_granted_from_xml
-
# authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
-
#
-
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
-
#
-
# assert_equal 200, status
-
# end
-
#
-
#
-
# On shared hosts, Apache sometimes doesn't pass authentication headers to
-
# FCGI instances. If your environment matches this description and you cannot
-
# authenticate, try this rule in your Apache setup:
-
#
-
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
-
1
module Token
-
1
TOKEN_KEY = "token="
-
1
TOKEN_REGEX = /^(Token|Bearer)\s+/
-
1
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
-
1
extend self
-
-
1
module ControllerMethods
-
1
def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
-
authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
-
end
-
-
1
def authenticate_with_http_token(&login_procedure)
-
Token.authenticate(self, &login_procedure)
-
end
-
-
1
def request_http_token_authentication(realm = "Application", message = nil)
-
Token.authentication_request(self, realm, message)
-
end
-
end
-
-
# If token Authorization header is present, call the login
-
# procedure with the present token and options.
-
#
-
# [controller]
-
# ActionController::Base instance for the current request.
-
#
-
# [login_procedure]
-
# Proc to call if a token is present. The Proc should take two arguments:
-
#
-
# authenticate(controller) { |token, options| ... }
-
#
-
# Returns the return value of <tt>login_procedure</tt> if a
-
# token is found. Returns <tt>nil</tt> if no token is found.
-
-
1
def authenticate(controller, &login_procedure)
-
token, options = token_and_options(controller.request)
-
unless token.blank?
-
login_procedure.call(token, options)
-
end
-
end
-
-
# Parses the token and options out of the token Authorization header.
-
# The value for the Authorization header is expected to have the prefix
-
# <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
-
# Authorization: Token token="abc", nonce="def"
-
# Then the returned token is <tt>"abc"</tt>, and the options are
-
# <tt>{nonce: "def"}</tt>
-
#
-
# request - ActionDispatch::Request instance with the current headers.
-
#
-
# Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
-
# Returns +nil+ if no token is found.
-
1
def token_and_options(request)
-
authorization_request = request.authorization.to_s
-
if authorization_request[TOKEN_REGEX]
-
params = token_params_from authorization_request
-
[params.shift[1], Hash[params].with_indifferent_access]
-
end
-
end
-
-
1
def token_params_from(auth)
-
rewrite_param_values params_array_from raw_params auth
-
end
-
-
# Takes raw_params and turns it into an array of parameters
-
1
def params_array_from(raw_params)
-
raw_params.map { |param| param.split %r/=(.+)?/ }
-
end
-
-
# This removes the <tt>"</tt> characters wrapping the value.
-
1
def rewrite_param_values(array_params)
-
array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
-
end
-
-
# This method takes an authorization body and splits up the key-value
-
# pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
-
# delimiters defined in +AUTHN_PAIR_DELIMITERS+.
-
1
def raw_params(auth)
-
_raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
-
-
if !_raw_params.first&.start_with?(TOKEN_KEY)
-
_raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
-
end
-
-
_raw_params
-
end
-
-
# Encodes the given token and options into an Authorization header value.
-
#
-
# token - String token.
-
# options - optional Hash of the options.
-
#
-
# Returns String.
-
1
def encode_credentials(token, options = {})
-
values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
-
"#{key}=#{value.to_s.inspect}"
-
end
-
"Token #{values * ", "}"
-
end
-
-
# Sets a WWW-Authenticate header to let the client know a token is desired.
-
#
-
# controller - ActionController::Base instance for the outgoing response.
-
# realm - String realm to use in the header.
-
#
-
# Returns nothing.
-
1
def authentication_request(controller, realm, message = nil)
-
message ||= "HTTP Token: Access denied.\n"
-
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
-
controller.__send__ :render, plain: message, status: :unauthorized
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# Handles implicit rendering for a controller action that does not
-
# explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
-
#
-
# For API controllers, the implicit response is always <tt>204 No Content</tt>.
-
#
-
# For all other controllers, we use these heuristics to decide whether to
-
# render a template, raise an error for a missing template, or respond with
-
# <tt>204 No Content</tt>:
-
#
-
# First, if we DO find a template, it's rendered. Template lookup accounts
-
# for the action name, locales, format, variant, template handlers, and more
-
# (see +render+ for details).
-
#
-
# Second, if we DON'T find a template but the controller action does have
-
# templates for other formats, variants, etc., then we trust that you meant
-
# to provide a template for this response, too, and we raise
-
# <tt>ActionController::UnknownFormat</tt> with an explanation.
-
#
-
# Third, if we DON'T find a template AND the request is a page load in a web
-
# browser (technically, a non-XHR GET request for an HTML response) where
-
# you reasonably expect to have rendered a template, then we raise
-
# <tt>ActionController::MissingExactTemplate</tt> with an explanation.
-
#
-
# Finally, if we DON'T find a template AND the request isn't a browser page
-
# load, then we implicitly respond with <tt>204 No Content</tt>.
-
1
module ImplicitRender
-
# :stopdoc:
-
1
include BasicImplicitRender
-
-
1
def default_render
-
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
-
render
-
elsif any_templates?(action_name.to_s, _prefixes)
-
message = "#{self.class.name}\##{action_name} is missing a template " \
-
"for this request format and variant.\n" \
-
"\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
-
"\nrequest.variant: #{request.variant.inspect}"
-
-
raise ActionController::UnknownFormat, message
-
elsif interactive_browser_request?
-
message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
-
raise ActionController::MissingExactTemplate, message
-
else
-
logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
-
super
-
end
-
end
-
-
1
def method_for_action(action_name)
-
super || if template_exists?(action_name.to_s, _prefixes)
-
"default_render"
-
end
-
end
-
-
1
private
-
1
def interactive_browser_request?
-
request.get? && request.format == Mime[:html] && !request.xhr?
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "benchmark"
-
1
require "abstract_controller/logger"
-
-
1
module ActionController
-
# Adds instrumentation to several ends in ActionController::Base. It also provides
-
# some hooks related with process_action. This allows an ORM like Active Record
-
# and/or DataMapper to plug in ActionController and show related information.
-
#
-
# Check ActiveRecord::Railties::ControllerRuntime for an example.
-
1
module Instrumentation
-
1
extend ActiveSupport::Concern
-
-
1
include AbstractController::Logger
-
-
1
attr_internal :view_runtime
-
-
1
def process_action(*)
-
raw_payload = {
-
controller: self.class.name,
-
action: action_name,
-
request: request,
-
params: request.filtered_parameters,
-
headers: request.headers,
-
format: request.format.ref,
-
method: request.request_method,
-
path: request.fullpath
-
}
-
-
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
-
-
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
-
result = super
-
payload[:response] = response
-
payload[:status] = response.status
-
result
-
ensure
-
append_info_to_payload(payload)
-
end
-
end
-
-
1
def render(*)
-
render_output = nil
-
self.view_runtime = cleanup_view_runtime do
-
Benchmark.ms { render_output = super }
-
end
-
render_output
-
end
-
-
1
def send_file(path, options = {})
-
ActiveSupport::Notifications.instrument("send_file.action_controller",
-
options.merge(path: path)) do
-
super
-
end
-
end
-
-
1
def send_data(data, options = {})
-
ActiveSupport::Notifications.instrument("send_data.action_controller", options) do
-
super
-
end
-
end
-
-
1
def redirect_to(*)
-
ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: request) do |payload|
-
result = super
-
payload[:status] = response.status
-
payload[:location] = response.filtered_location
-
result
-
end
-
end
-
-
1
private
-
# A hook invoked every time a before callback is halted.
-
1
def halted_callback_hook(filter, _)
-
ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
-
end
-
-
# A hook which allows you to clean up any time, wrongly taken into account in
-
# views, like database querying time.
-
#
-
# def cleanup_view_runtime
-
# super - time_taken_in_something_expensive
-
# end
-
1
def cleanup_view_runtime # :doc:
-
yield
-
end
-
-
# Every time after an action is processed, this method is invoked
-
# with the payload, so you can add more information.
-
1
def append_info_to_payload(payload) # :doc:
-
payload[:view_runtime] = view_runtime
-
end
-
-
1
module ClassMethods
-
# A hook which allows other frameworks to log what happened during
-
# controller process action. This method should return an array
-
# with the messages to be added.
-
1
def log_process_action(payload) #:nodoc:
-
messages, view_runtime = [], payload[:view_runtime]
-
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
-
messages
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/http/response"
-
1
require "delegate"
-
1
require "active_support/json"
-
-
1
module ActionController
-
# Mix this module into your controller, and all actions in that controller
-
# will be able to stream data to the client as it's written.
-
#
-
# class MyController < ActionController::Base
-
# include ActionController::Live
-
#
-
# def stream
-
# response.headers['Content-Type'] = 'text/event-stream'
-
# 100.times {
-
# response.stream.write "hello world\n"
-
# sleep 1
-
# }
-
# ensure
-
# response.stream.close
-
# end
-
# end
-
#
-
# There are a few caveats with this module. You *cannot* write headers after the
-
# response has been committed (Response#committed? will return truthy).
-
# Calling +write+ or +close+ on the response stream will cause the response
-
# object to be committed. Make sure all headers are set before calling write
-
# or close on your stream.
-
#
-
# You *must* call close on your stream when you're finished, otherwise the
-
# socket may be left open forever.
-
#
-
# The final caveat is that your actions are executed in a separate thread than
-
# the main thread. Make sure your actions are thread safe, and this shouldn't
-
# be a problem (don't share state across threads, etc).
-
1
module Live
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
1
def make_response!(request)
-
if request.get_header("HTTP_VERSION") == "HTTP/1.0"
-
super
-
else
-
Live::Response.new.tap do |res|
-
res.request = request
-
end
-
end
-
end
-
end
-
-
# This class provides the ability to write an SSE (Server Sent Event)
-
# to an IO stream. The class is initialized with a stream and can be used
-
# to either write a JSON string or an object which can be converted to JSON.
-
#
-
# Writing an object will convert it into standard SSE format with whatever
-
# options you have configured. You may choose to set the following options:
-
#
-
# 1) Event. If specified, an event with this name will be dispatched on
-
# the browser.
-
# 2) Retry. The reconnection time in milliseconds used when attempting
-
# to send the event.
-
# 3) Id. If the connection dies while sending an SSE to the browser, then
-
# the server will receive a +Last-Event-ID+ header with value equal to +id+.
-
#
-
# After setting an option in the constructor of the SSE object, all future
-
# SSEs sent across the stream will use those options unless overridden.
-
#
-
# Example Usage:
-
#
-
# class MyController < ActionController::Base
-
# include ActionController::Live
-
#
-
# def index
-
# response.headers['Content-Type'] = 'text/event-stream'
-
# sse = SSE.new(response.stream, retry: 300, event: "event-name")
-
# sse.write({ name: 'John'})
-
# sse.write({ name: 'John'}, id: 10)
-
# sse.write({ name: 'John'}, id: 10, event: "other-event")
-
# sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
-
# ensure
-
# sse.close
-
# end
-
# end
-
#
-
# Note: SSEs are not currently supported by IE. However, they are supported
-
# by Chrome, Firefox, Opera, and Safari.
-
1
class SSE
-
1
PERMITTED_OPTIONS = %w( retry event id )
-
-
1
def initialize(stream, options = {})
-
@stream = stream
-
@options = options
-
end
-
-
1
def close
-
@stream.close
-
end
-
-
1
def write(object, options = {})
-
case object
-
when String
-
perform_write(object, options)
-
else
-
perform_write(ActiveSupport::JSON.encode(object), options)
-
end
-
end
-
-
1
private
-
1
def perform_write(json, options)
-
current_options = @options.merge(options).stringify_keys
-
-
PERMITTED_OPTIONS.each do |option_name|
-
if (option_value = current_options[option_name])
-
@stream.write "#{option_name}: #{option_value}\n"
-
end
-
end
-
-
message = json.gsub("\n", "\ndata: ")
-
@stream.write "data: #{message}\n\n"
-
end
-
end
-
-
1
class ClientDisconnected < RuntimeError
-
end
-
-
1
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
-
1
include MonitorMixin
-
-
# Ignore that the client has disconnected.
-
#
-
# If this value is `true`, calling `write` after the client
-
# disconnects will result in the written content being silently
-
# discarded. If this value is `false` (the default), a
-
# ClientDisconnected exception will be raised.
-
1
attr_accessor :ignore_disconnect
-
-
1
def initialize(response)
-
super(response, SizedQueue.new(10))
-
@error_callback = lambda { true }
-
@cv = new_cond
-
@aborted = false
-
@ignore_disconnect = false
-
end
-
-
1
def write(string)
-
unless @response.committed?
-
@response.headers["Cache-Control"] ||= "no-cache"
-
@response.delete_header "Content-Length"
-
end
-
-
super
-
-
unless connected?
-
@buf.clear
-
-
unless @ignore_disconnect
-
# Raise ClientDisconnected, which is a RuntimeError (not an
-
# IOError), because that's more appropriate for something beyond
-
# the developer's control.
-
raise ClientDisconnected, "client disconnected"
-
end
-
end
-
end
-
-
# Write a 'close' event to the buffer; the producer/writing thread
-
# uses this to notify us that it's finished supplying content.
-
#
-
# See also #abort.
-
1
def close
-
synchronize do
-
super
-
@buf.push nil
-
@cv.broadcast
-
end
-
end
-
-
# Inform the producer/writing thread that the client has
-
# disconnected; the reading thread is no longer interested in
-
# anything that's being written.
-
#
-
# See also #close.
-
1
def abort
-
synchronize do
-
@aborted = true
-
@buf.clear
-
end
-
end
-
-
# Is the client still connected and waiting for content?
-
#
-
# The result of calling `write` when this is `false` is determined
-
# by `ignore_disconnect`.
-
1
def connected?
-
!@aborted
-
end
-
-
1
def on_error(&block)
-
@error_callback = block
-
end
-
-
1
def call_on_error
-
@error_callback.call
-
end
-
-
1
private
-
1
def each_chunk(&block)
-
loop do
-
str = nil
-
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
-
str = @buf.pop
-
end
-
break unless str
-
yield str
-
end
-
end
-
end
-
-
1
class Response < ActionDispatch::Response #:nodoc: all
-
1
private
-
1
def before_committed
-
super
-
jar = request.cookie_jar
-
# The response can be committed multiple times
-
jar.write self unless committed?
-
end
-
-
1
def build_buffer(response, body)
-
buf = Live::Buffer.new response
-
body.each { |part| buf.write part }
-
buf
-
end
-
end
-
-
1
def process(name)
-
t1 = Thread.current
-
locals = t1.keys.map { |key| [key, t1[key]] }
-
-
error = nil
-
# This processes the action in a child thread. It lets us return the
-
# response code and headers back up the Rack stack, and still process
-
# the body in parallel with sending data to the client.
-
new_controller_thread {
-
ActiveSupport::Dependencies.interlock.running do
-
t2 = Thread.current
-
-
# Since we're processing the view in a different thread, copy the
-
# thread locals from the main thread to the child thread. :'(
-
locals.each { |k, v| t2[k] = v }
-
-
begin
-
super(name)
-
rescue => e
-
if @_response.committed?
-
begin
-
@_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
-
@_response.stream.call_on_error
-
rescue => exception
-
log_error(exception)
-
ensure
-
log_error(e)
-
@_response.stream.close
-
end
-
else
-
error = e
-
end
-
ensure
-
@_response.commit!
-
end
-
end
-
}
-
-
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
-
@_response.await_commit
-
end
-
-
raise error if error
-
end
-
-
1
def response_body=(body)
-
super
-
response.close if response
-
end
-
-
1
private
-
# Spawn a new thread to serve up the controller in. This is to get
-
# around the fact that Rack isn't based around IOs and we need to use
-
# a thread to stream data from the response bodies. Nobody should call
-
# this method except in Rails internals. Seriously!
-
1
def new_controller_thread # :nodoc:
-
Thread.new {
-
t2 = Thread.current
-
t2.abort_on_exception = true
-
yield
-
}
-
end
-
-
1
def log_error(exception)
-
logger = ActionController::Base.logger
-
return unless logger
-
-
logger.fatal do
-
message = +"\n#{exception.class} (#{exception.message}):\n"
-
message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
-
message << " " << exception.backtrace.join("\n ")
-
"#{message}\n\n"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module Logging
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
# Set a different log level per request.
-
#
-
# # Use the debug log level if a particular cookie is set.
-
# class ApplicationController < ActionController::Base
-
# log_at :debug, if: -> { cookies[:debug] }
-
# end
-
#
-
1
def log_at(level, **options)
-
around_action ->(_, action) { logger.log_at(level, &action) }, **options
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "abstract_controller/collector"
-
-
1
module ActionController #:nodoc:
-
1
module MimeResponds
-
# Without web-service support, an action which collects the data for displaying a list of people
-
# might look something like this:
-
#
-
# def index
-
# @people = Person.all
-
# end
-
#
-
# That action implicitly responds to all formats, but formats can also be explicitly enumerated:
-
#
-
# def index
-
# @people = Person.all
-
# respond_to :html, :js
-
# end
-
#
-
# Here's the same action, with web-service support baked in:
-
#
-
# def index
-
# @people = Person.all
-
#
-
# respond_to do |format|
-
# format.html
-
# format.js
-
# format.xml { render xml: @people }
-
# end
-
# end
-
#
-
# What that says is, "if the client wants HTML or JS in response to this action, just respond as we
-
# would have before, but if the client wants XML, return them the list of people in XML format."
-
# (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
-
#
-
# Supposing you have an action that adds a new person, optionally creating their company
-
# (by name) if it does not already exist, without web-services, it might look like this:
-
#
-
# def create
-
# @company = Company.find_or_create_by(name: params[:company][:name])
-
# @person = @company.people.create(params[:person])
-
#
-
# redirect_to(person_list_url)
-
# end
-
#
-
# Here's the same action, with web-service support baked in:
-
#
-
# def create
-
# company = params[:person].delete(:company)
-
# @company = Company.find_or_create_by(name: company[:name])
-
# @person = @company.people.create(params[:person])
-
#
-
# respond_to do |format|
-
# format.html { redirect_to(person_list_url) }
-
# format.js
-
# format.xml { render xml: @person.to_xml(include: @company) }
-
# end
-
# end
-
#
-
# If the client wants HTML, we just redirect them back to the person list. If they want JavaScript,
-
# then it is an Ajax request and we render the JavaScript template associated with this action.
-
# Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
-
# include the person's company in the rendered XML, so you get something like this:
-
#
-
# <person>
-
# <id>...</id>
-
# ...
-
# <company>
-
# <id>...</id>
-
# <name>...</name>
-
# ...
-
# </company>
-
# </person>
-
#
-
# Note, however, the extra bit at the top of that action:
-
#
-
# company = params[:person].delete(:company)
-
# @company = Company.find_or_create_by(name: company[:name])
-
#
-
# This is because the incoming XML document (if a web-service request is in process) can only contain a
-
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
-
#
-
# person[name]=...&person[company][name]=...&...
-
#
-
# And, like this (xml-encoded):
-
#
-
# <person>
-
# <name>...</name>
-
# <company>
-
# <name>...</name>
-
# </company>
-
# </person>
-
#
-
# In other words, we make the request so that it operates on a single entity's person. Then, in the action,
-
# we extract the company data from the request, find or create the company, and then create the new person
-
# with the remaining data.
-
#
-
# Note that you can define your own XML parameter parser which would allow you to describe multiple entities
-
# in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
-
# and accept Rails' defaults, life will be much easier.
-
#
-
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
-
# +config/initializers/mime_types.rb+ as follows.
-
#
-
# Mime::Type.register "image/jpg", :jpg
-
#
-
# +respond_to+ also allows you to specify a common block for different formats by using +any+:
-
#
-
# def index
-
# @people = Person.all
-
#
-
# respond_to do |format|
-
# format.html
-
# format.any(:xml, :json) { render request.format.to_sym => @people }
-
# end
-
# end
-
#
-
# In the example above, if the format is xml, it will render:
-
#
-
# render xml: @people
-
#
-
# Or if the format is json:
-
#
-
# render json: @people
-
#
-
# +any+ can also be used with no arguments, in which case it will be used for any format requested by
-
# the user:
-
#
-
# respond_to do |format|
-
# format.html
-
# format.any { redirect_to support_path }
-
# end
-
#
-
# Formats can have different variants.
-
#
-
# The request variant is a specialization of the request format, like <tt>:tablet</tt>,
-
# <tt>:phone</tt>, or <tt>:desktop</tt>.
-
#
-
# We often want to render different html/json/xml templates for phones,
-
# tablets, and desktop browsers. Variants make it easy.
-
#
-
# You can set the variant in a +before_action+:
-
#
-
# request.variant = :tablet if /iPad/.match?(request.user_agent)
-
#
-
# Respond to variants in the action just like you respond to formats:
-
#
-
# respond_to do |format|
-
# format.html do |variant|
-
# variant.tablet # renders app/views/projects/show.html+tablet.erb
-
# variant.phone { extra_setup; render ... }
-
# variant.none { special_setup } # executed only if there is no variant set
-
# end
-
# end
-
#
-
# Provide separate templates for each format and variant:
-
#
-
# app/views/projects/show.html.erb
-
# app/views/projects/show.html+tablet.erb
-
# app/views/projects/show.html+phone.erb
-
#
-
# When you're not sharing any code within the format, you can simplify defining variants
-
# using the inline syntax:
-
#
-
# respond_to do |format|
-
# format.js { render "trash" }
-
# format.html.phone { redirect_to progress_path }
-
# format.html.none { render "trash" }
-
# end
-
#
-
# Variants also support common +any+/+all+ block that formats have.
-
#
-
# It works for both inline:
-
#
-
# respond_to do |format|
-
# format.html.any { render html: "any" }
-
# format.html.phone { render html: "phone" }
-
# end
-
#
-
# and block syntax:
-
#
-
# respond_to do |format|
-
# format.html do |variant|
-
# variant.any(:tablet, :phablet){ render html: "any" }
-
# variant.phone { render html: "phone" }
-
# end
-
# end
-
#
-
# You can also set an array of variants:
-
#
-
# request.variant = [:tablet, :phone]
-
#
-
# This will work similarly to formats and MIME types negotiation. If there
-
# is no +:tablet+ variant declared, the +:phone+ variant will be used:
-
#
-
# respond_to do |format|
-
# format.html.none
-
# format.html.phone # this gets rendered
-
# end
-
1
def respond_to(*mimes)
-
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
-
-
collector = Collector.new(mimes, request.variant)
-
yield collector if block_given?
-
-
if format = collector.negotiate_format(request)
-
if media_type && media_type != format
-
raise ActionController::RespondToMismatchError
-
end
-
_process_format(format)
-
_set_rendered_content_type(format) unless collector.any_response?
-
response = collector.response
-
response.call if response
-
else
-
raise ActionController::UnknownFormat
-
end
-
end
-
-
# A container for responses available from the current controller for
-
# requests for different mime-types sent to a particular action.
-
#
-
# The public controller methods +respond_to+ may be called with a block
-
# that is used to define responses to different mime-types, e.g.
-
# for +respond_to+ :
-
#
-
# respond_to do |format|
-
# format.html
-
# format.xml { render xml: @people }
-
# end
-
#
-
# In this usage, the argument passed to the block (+format+ above) is an
-
# instance of the ActionController::MimeResponds::Collector class. This
-
# object serves as a container in which available responses can be stored by
-
# calling any of the dynamically generated, mime-type-specific methods such
-
# as +html+, +xml+ etc on the Collector. Each response is represented by a
-
# corresponding block if present.
-
#
-
# A subsequent call to #negotiate_format(request) will enable the Collector
-
# to determine which specific mime-type it should respond with for the current
-
# request, with this response then being accessible by calling #response.
-
1
class Collector
-
1
include AbstractController::Collector
-
1
attr_accessor :format
-
-
1
def initialize(mimes, variant = nil)
-
@responses = {}
-
@variant = variant
-
-
mimes.each { |mime| @responses[Mime[mime]] = nil }
-
end
-
-
1
def any(*args, &block)
-
if args.any?
-
args.each { |type| send(type, &block) }
-
else
-
custom(Mime::ALL, &block)
-
end
-
end
-
1
alias :all :any
-
-
1
def custom(mime_type, &block)
-
mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
-
@responses[mime_type] ||= if block_given?
-
block
-
else
-
VariantCollector.new(@variant)
-
end
-
end
-
-
1
def any_response?
-
!@responses.fetch(format, false) && @responses[Mime::ALL]
-
end
-
-
1
def response
-
response = @responses.fetch(format, @responses[Mime::ALL])
-
if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
-
response.variant
-
elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
-
response
-
else # `format.html{ |variant| variant.phone }` - variant block syntax
-
variant_collector = VariantCollector.new(@variant)
-
response.call(variant_collector) # call format block with variants collector
-
variant_collector.variant
-
end
-
end
-
-
1
def negotiate_format(request)
-
@format = request.negotiate_mime(@responses.keys)
-
end
-
-
1
class VariantCollector #:nodoc:
-
1
def initialize(variant = nil)
-
@variant = variant
-
@variants = {}
-
end
-
-
1
def any(*args, &block)
-
if block_given?
-
if args.any? && args.none? { |a| a == @variant }
-
args.each { |v| @variants[v] = block }
-
else
-
@variants[:any] = block
-
end
-
end
-
end
-
1
alias :all :any
-
-
1
def method_missing(name, *args, &block)
-
@variants[name] = block if block_given?
-
end
-
-
1
def variant
-
if @variant.empty?
-
@variants[:none] || @variants[:any]
-
else
-
@variants[variant_key]
-
end
-
end
-
-
1
private
-
1
def variant_key
-
@variant.find { |variant| @variants.key?(variant) } || :any
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# Specify binary encoding for parameters for a given action.
-
1
module ParameterEncoding
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
1
def inherited(klass) # :nodoc:
-
1
super
-
1
klass.setup_param_encode
-
end
-
-
1
def setup_param_encode # :nodoc:
-
1
@_parameter_encodings = Hash.new { |h, k| h[k] = {} }
-
end
-
-
1
def action_encoding_template(action) # :nodoc:
-
if @_parameter_encodings.has_key?(action.to_s)
-
@_parameter_encodings[action.to_s]
-
end
-
end
-
-
# Specify that a given action's parameters should all be encoded as
-
# ASCII-8BIT (it "skips" the encoding default of UTF-8).
-
#
-
# For example, a controller would use it like this:
-
#
-
# class RepositoryController < ActionController::Base
-
# skip_parameter_encoding :show
-
#
-
# def show
-
# @repo = Repository.find_by_filesystem_path params[:file_path]
-
#
-
# # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so
-
# # tag it as such
-
# @repo_name = params[:repo_name].force_encoding 'UTF-8'
-
# end
-
#
-
# def index
-
# @repositories = Repository.all
-
# end
-
# end
-
#
-
# The show action in the above controller would have all parameter values
-
# encoded as ASCII-8BIT. This is useful in the case where an application
-
# must handle data but encoding of the data is unknown, like file system data.
-
1
def skip_parameter_encoding(action)
-
@_parameter_encodings[action.to_s] = Hash.new { Encoding::ASCII_8BIT }
-
end
-
-
# Specify the encoding for a parameter on an action.
-
# If not specified the default is UTF-8.
-
#
-
# You can specify a binary (ASCII_8BIT) parameter with:
-
#
-
# class RepositoryController < ActionController::Base
-
# # This specifies that file_path is not UTF-8 and is instead ASCII_8BIT
-
# param_encoding :show, :file_path, Encoding::ASCII_8BIT
-
#
-
# def show
-
# @repo = Repository.find_by_filesystem_path params[:file_path]
-
#
-
# # params[:repo_name] remains UTF-8 encoded
-
# @repo_name = params[:repo_name]
-
# end
-
#
-
# def index
-
# @repositories = Repository.all
-
# end
-
# end
-
#
-
# The file_path parameter on the show action would be encoded as ASCII-8BIT,
-
# but all other arguments will remain UTF-8 encoded.
-
# This is useful in the case where an application must handle data
-
# but encoding of the data is unknown, like file system data.
-
1
def param_encoding(action, param, encoding)
-
@_parameter_encodings[action.to_s][param.to_s] = encoding
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/slice"
-
1
require "active_support/core_ext/hash/except"
-
1
require "active_support/core_ext/module/anonymous"
-
1
require "action_dispatch/http/mime_type"
-
-
1
module ActionController
-
# Wraps the parameters hash into a nested hash. This will allow clients to
-
# submit requests without having to specify any root elements.
-
#
-
# This functionality is enabled in +config/initializers/wrap_parameters.rb+
-
# and can be customized.
-
#
-
# You could also turn it on per controller by setting the format array to
-
# a non-empty array:
-
#
-
# class UsersController < ApplicationController
-
# wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
-
# end
-
#
-
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to
-
# send JSON parameters like this:
-
#
-
# {"user": {"name": "Konata"}}
-
#
-
# You can send parameters like this:
-
#
-
# {"name": "Konata"}
-
#
-
# And it will be wrapped into a nested hash with the key name matching the
-
# controller's name. For example, if you're posting to +UsersController+,
-
# your new +params+ hash will look like this:
-
#
-
# {"name" => "Konata", "user" => {"name" => "Konata"}}
-
#
-
# You can also specify the key in which the parameters should be wrapped to,
-
# and also the list of attributes it should wrap by using either +:include+ or
-
# +:exclude+ options like this:
-
#
-
# class UsersController < ApplicationController
-
# wrap_parameters :person, include: [:username, :password]
-
# end
-
#
-
# On Active Record models with no +:include+ or +:exclude+ option set,
-
# it will only wrap the parameters returned by the class method
-
# <tt>attribute_names</tt>.
-
#
-
# If you're going to pass the parameters to an +ActiveModel+ object (such as
-
# <tt>User.new(params[:user])</tt>), you might consider passing the model class to
-
# the method instead. The +ParamsWrapper+ will actually try to determine the
-
# list of attribute names from the model and only wrap those attributes:
-
#
-
# class UsersController < ApplicationController
-
# wrap_parameters Person
-
# end
-
#
-
# You still could pass +:include+ and +:exclude+ to set the list of attributes
-
# you want to wrap.
-
#
-
# By default, if you don't specify the key in which the parameters would be
-
# wrapped to, +ParamsWrapper+ will actually try to determine if there's
-
# a model related to it or not. This controller, for example:
-
#
-
# class Admin::UsersController < ApplicationController
-
# end
-
#
-
# will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
-
# determine the wrapper key respectively. If both models don't exist,
-
# it will then fallback to use +user+ as the key.
-
1
module ParamsWrapper
-
1
extend ActiveSupport::Concern
-
-
1
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
-
-
1
require "mutex_m"
-
-
1
class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
-
1
include Mutex_m
-
-
1
def self.from_hash(hash)
-
2
name = hash[:name]
-
2
format = Array(hash[:format])
-
2
include = hash[:include] && Array(hash[:include]).collect(&:to_s)
-
2
exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
-
2
new name, format, include, exclude, nil, nil
-
end
-
-
1
def initialize(name, format, include, exclude, klass, model) # :nodoc:
-
2
super
-
2
@include_set = include
-
2
@name_set = name
-
end
-
-
1
def model
-
super || self.model = _default_wrap_model
-
end
-
-
1
def include
-
return super if @include_set
-
-
m = model
-
synchronize do
-
return super if @include_set
-
-
@include_set = true
-
-
unless super || exclude
-
if m.respond_to?(:attribute_names) && m.attribute_names.any?
-
self.include = m.attribute_names
-
-
if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
-
self.include += m.stored_attributes.values.flatten.map(&:to_s)
-
end
-
-
if m.respond_to?(:attribute_aliases) && m.attribute_aliases.any?
-
self.include += m.attribute_aliases.keys
-
end
-
-
if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
-
self.include += m.nested_attributes_options.keys.map do |key|
-
(+key.to_s).concat("_attributes")
-
end
-
end
-
-
self.include
-
end
-
end
-
end
-
end
-
-
1
def name
-
return super if @name_set
-
-
m = model
-
synchronize do
-
return super if @name_set
-
-
@name_set = true
-
-
unless super || klass.anonymous?
-
self.name = m ? m.to_s.demodulize.underscore :
-
klass.controller_name.singularize
-
end
-
end
-
end
-
-
1
private
-
# Determine the wrapper model from the controller's name. By convention,
-
# this could be done by trying to find the defined model that has the
-
# same singular name as the controller. For example, +UsersController+
-
# will try to find if the +User+ model exists.
-
#
-
# This method also does namespace lookup. Foo::Bar::UsersController will
-
# try to find Foo::Bar::User, Foo::User and finally User.
-
1
def _default_wrap_model
-
return nil if klass.anonymous?
-
model_name = klass.name.delete_suffix("Controller").classify
-
-
begin
-
if model_klass = model_name.safe_constantize
-
model_klass
-
else
-
namespaces = model_name.split("::")
-
namespaces.delete_at(-2)
-
break if namespaces.last == model_name
-
model_name = namespaces.join("::")
-
end
-
end until model_klass
-
-
model_klass
-
end
-
end
-
-
1
included do
-
1
class_attribute :_wrapper_options, default: Options.from_hash(format: [])
-
end
-
-
1
module ClassMethods
-
1
def _set_wrapper_options(options)
-
self._wrapper_options = Options.from_hash(options)
-
end
-
-
# Sets the name of the wrapper key, or the model which +ParamsWrapper+
-
# would use to determine the attribute names from.
-
#
-
# ==== Examples
-
# wrap_parameters format: :xml
-
# # enables the parameter wrapper for XML format
-
#
-
# wrap_parameters :person
-
# # wraps parameters into +params[:person]+ hash
-
#
-
# wrap_parameters Person
-
# # wraps parameters by determining the wrapper key from Person class
-
# # (+person+, in this case) and the list of attribute names
-
#
-
# wrap_parameters include: [:username, :title]
-
# # wraps only +:username+ and +:title+ attributes from parameters.
-
#
-
# wrap_parameters false
-
# # disables parameters wrapping for this controller altogether.
-
#
-
# ==== Options
-
# * <tt>:format</tt> - The list of formats in which the parameters wrapper
-
# will be enabled.
-
# * <tt>:include</tt> - The list of attribute names which parameters wrapper
-
# will wrap into a nested hash.
-
# * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
-
# will exclude from a nested hash.
-
1
def wrap_parameters(name_or_model_or_options, options = {})
-
1
model = nil
-
-
1
case name_or_model_or_options
-
when Hash
-
1
options = name_or_model_or_options
-
when false
-
options = options.merge(format: [])
-
when Symbol, String
-
options = options.merge(name: name_or_model_or_options)
-
else
-
model = name_or_model_or_options
-
end
-
-
1
opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
-
1
opts.model = model
-
1
opts.klass = self
-
-
1
self._wrapper_options = opts
-
end
-
-
# Sets the default wrapper key or model which will be used to determine
-
# wrapper key and attribute names. Called automatically when the
-
# module is inherited.
-
1
def inherited(klass)
-
1
if klass._wrapper_options.format.any?
-
1
params = klass._wrapper_options.dup
-
1
params.klass = klass
-
1
klass._wrapper_options = params
-
end
-
1
super
-
end
-
end
-
-
# Performs parameters wrapping upon the request. Called automatically
-
# by the metal call stack.
-
1
def process_action(*)
-
_perform_parameter_wrapping if _wrapper_enabled?
-
super
-
end
-
-
1
private
-
# Returns the wrapper key which will be used to store wrapped parameters.
-
1
def _wrapper_key
-
_wrapper_options.name
-
end
-
-
# Returns the list of enabled formats.
-
1
def _wrapper_formats
-
_wrapper_options.format
-
end
-
-
# Returns the list of parameters which will be selected for wrapped.
-
1
def _wrap_parameters(parameters)
-
{ _wrapper_key => _extract_parameters(parameters) }
-
end
-
-
1
def _extract_parameters(parameters)
-
if include_only = _wrapper_options.include
-
parameters.slice(*include_only)
-
elsif _wrapper_options.exclude
-
exclude = _wrapper_options.exclude + EXCLUDE_PARAMETERS
-
parameters.except(*exclude)
-
else
-
parameters.except(*EXCLUDE_PARAMETERS)
-
end
-
end
-
-
# Checks if we should perform parameters wrapping.
-
1
def _wrapper_enabled?
-
return false unless request.has_content_type?
-
-
ref = request.content_mime_type.ref
-
_wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
-
end
-
-
1
def _perform_parameter_wrapping
-
wrapped_hash = _wrap_parameters request.request_parameters
-
wrapped_keys = request.request_parameters.keys
-
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
-
-
# This will make the wrapped hash accessible from controller and view.
-
request.parameters.merge! wrapped_hash
-
request.request_parameters.merge! wrapped_hash
-
-
# This will display the wrapped hash in the log file.
-
request.filtered_parameters.merge! wrapped_filtered_hash
-
rescue ActionDispatch::Http::Parameters::ParseError
-
# swallow parse error exception
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController #:nodoc:
-
# HTTP Permissions Policy is a web standard for defining a mechanism to
-
# allow and deny the use of browser permissions in its own context, and
-
# in content within any <iframe> elements in the document.
-
#
-
# Full details of HTTP Permissions Policy specification and guidelines can
-
# be found at MDN:
-
#
-
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
-
#
-
# Examples of usage:
-
#
-
# # Global policy
-
# Rails.application.config.permissions_policy do |f|
-
# f.camera :none
-
# f.gyroscope :none
-
# f.microphone :none
-
# f.usb :none
-
# f.fullscreen :self
-
# f.payment :self, "https://secure.example.com"
-
# end
-
#
-
# # Controller level policy
-
# class PagesController < ApplicationController
-
# permissions_policy do |p|
-
# p.geolocation "https://example.com"
-
# end
-
# end
-
1
module PermissionsPolicy
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
1
def permissions_policy(**options, &block)
-
before_action(options) do
-
if block_given?
-
policy = request.permissions_policy.clone
-
yield policy
-
request.permissions_policy = policy
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module Redirecting
-
1
extend ActiveSupport::Concern
-
-
1
include AbstractController::Logger
-
1
include ActionController::UrlFor
-
-
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
-
#
-
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
-
# * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
-
# * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
-
# * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
-
# * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
-
#
-
# === Examples:
-
#
-
# redirect_to action: "show", id: 5
-
# redirect_to @post
-
# redirect_to "http://www.rubyonrails.org"
-
# redirect_to "/images/screenshot.jpg"
-
# redirect_to posts_url
-
# redirect_to proc { edit_post_url(@post) }
-
#
-
# The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
-
#
-
# redirect_to post_url(@post), status: :found
-
# redirect_to action: 'atom', status: :moved_permanently
-
# redirect_to post_url(@post), status: 301
-
# redirect_to action: 'atom', status: 302
-
#
-
# The status code can either be a standard {HTTP Status code}[https://www.iana.org/assignments/http-status-codes] as an
-
# integer, or a symbol representing the downcased, underscored and symbolized description.
-
# Note that the status code must be a 3xx HTTP code, or redirection will not occur.
-
#
-
# If you are using XHR requests other than GET or POST and redirecting after the
-
# request then some browsers will follow the redirect using the original request
-
# method. This may lead to undesirable behavior such as a double DELETE. To work
-
# around this you can return a <tt>303 See Other</tt> status code which will be
-
# followed using a GET request.
-
#
-
# redirect_to posts_url, status: :see_other
-
# redirect_to action: 'index', status: 303
-
#
-
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
-
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
-
#
-
# redirect_to post_url(@post), alert: "Watch it, mister!"
-
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
-
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
-
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
-
#
-
# Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
-
# To terminate the execution of the function immediately after the +redirect_to+, use return.
-
# redirect_to post_url(@post) and return
-
1
def redirect_to(options = {}, response_options = {})
-
raise ActionControllerError.new("Cannot redirect to nil!") unless options
-
raise AbstractController::DoubleRenderError if response_body
-
-
self.status = _extract_redirect_to_status(options, response_options)
-
self.location = _compute_redirect_to_location(request, options)
-
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
-
end
-
-
# Redirects the browser to the page that issued the request (the referrer)
-
# if possible, otherwise redirects to the provided default fallback
-
# location.
-
#
-
# The referrer information is pulled from the HTTP +Referer+ (sic) header on
-
# the request. This is an optional header and its presence on the request is
-
# subject to browser security settings and user preferences. If the request
-
# is missing this header, the <tt>fallback_location</tt> will be used.
-
#
-
# redirect_back fallback_location: { action: "show", id: 5 }
-
# redirect_back fallback_location: @post
-
# redirect_back fallback_location: "http://www.rubyonrails.org"
-
# redirect_back fallback_location: "/images/screenshot.jpg"
-
# redirect_back fallback_location: posts_url
-
# redirect_back fallback_location: proc { edit_post_url(@post) }
-
# redirect_back fallback_location: '/', allow_other_host: false
-
#
-
# ==== Options
-
# * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
-
# * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
-
#
-
# All other options that can be passed to #redirect_to are accepted as
-
# options and the behavior is identical.
-
1
def redirect_back(fallback_location:, allow_other_host: true, **args)
-
referer = request.headers["Referer"]
-
redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
-
redirect_to redirect_to_referer ? referer : fallback_location, **args
-
end
-
-
1
def _compute_redirect_to_location(request, options) #:nodoc:
-
case options
-
# The scheme name consist of a letter followed by any combination of
-
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
-
# characters; and is terminated by a colon (":").
-
# See https://tools.ietf.org/html/rfc3986#section-3.1
-
# The protocol relative scheme starts with a double slash "//".
-
when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
-
options
-
when String
-
request.protocol + request.host_with_port + options
-
when Proc
-
_compute_redirect_to_location request, instance_eval(&options)
-
else
-
url_for(options)
-
end.delete("\0\r\n")
-
end
-
1
module_function :_compute_redirect_to_location
-
1
public :_compute_redirect_to_location
-
-
1
private
-
1
def _extract_redirect_to_status(options, response_options)
-
if options.is_a?(Hash) && options.key?(:status)
-
Rack::Utils.status_code(options.delete(:status))
-
elsif response_options.key?(:status)
-
Rack::Utils.status_code(response_options[:status])
-
else
-
302
-
end
-
end
-
-
1
def _url_host_allowed?(url)
-
URI(url.to_s).host == request.host
-
rescue ArgumentError, URI::Error
-
false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "set"
-
-
1
module ActionController
-
# See <tt>Renderers.add</tt>
-
1
def self.add_renderer(key, &block)
-
Renderers.add(key, &block)
-
end
-
-
# See <tt>Renderers.remove</tt>
-
1
def self.remove_renderer(key)
-
Renderers.remove(key)
-
end
-
-
# See <tt>Responder#api_behavior</tt>
-
1
class MissingRenderer < LoadError
-
1
def initialize(format)
-
super "No renderer defined for format: #{format}"
-
end
-
end
-
-
1
module Renderers
-
1
extend ActiveSupport::Concern
-
-
# A Set containing renderer names that correspond to available renderer procs.
-
# Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
-
1
RENDERERS = Set.new
-
-
1
included do
-
1
class_attribute :_renderers, default: Set.new.freeze
-
end
-
-
# Used in <tt>ActionController::Base</tt>
-
# and <tt>ActionController::API</tt> to include all
-
# renderers by default.
-
1
module All
-
1
extend ActiveSupport::Concern
-
1
include Renderers
-
-
1
included do
-
1
self._renderers = RENDERERS
-
end
-
end
-
-
# Adds a new renderer to call within controller actions.
-
# A renderer is invoked by passing its name as an option to
-
# <tt>AbstractController::Rendering#render</tt>. To create a renderer
-
# pass it a name and a block. The block takes two arguments, the first
-
# is the value paired with its key and the second is the remaining
-
# hash of options passed to +render+.
-
#
-
# Create a csv renderer:
-
#
-
# ActionController::Renderers.add :csv do |obj, options|
-
# filename = options[:filename] || 'data'
-
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
-
# send_data str, type: Mime[:csv],
-
# disposition: "attachment; filename=#{filename}.csv"
-
# end
-
#
-
# Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
-
# For a custom renderer, you'll need to register a mime type with
-
# <tt>Mime::Type.register</tt>.
-
#
-
# To use the csv renderer in a controller action:
-
#
-
# def show
-
# @csvable = Csvable.find(params[:id])
-
# respond_to do |format|
-
# format.html
-
# format.csv { render csv: @csvable, filename: @csvable.name }
-
# end
-
# end
-
1
def self.add(key, &block)
-
3
define_method(_render_with_renderer_method_name(key), &block)
-
3
RENDERERS << key.to_sym
-
end
-
-
# This method is the opposite of add method.
-
#
-
# To remove a csv renderer:
-
#
-
# ActionController::Renderers.remove(:csv)
-
1
def self.remove(key)
-
RENDERERS.delete(key.to_sym)
-
method_name = _render_with_renderer_method_name(key)
-
remove_possible_method(method_name)
-
end
-
-
1
def self._render_with_renderer_method_name(key)
-
3
"_render_with_renderer_#{key}"
-
end
-
-
1
module ClassMethods
-
# Adds, by name, a renderer or renderers to the +_renderers+ available
-
# to call within controller actions.
-
#
-
# It is useful when rendering from an <tt>ActionController::Metal</tt> controller or
-
# otherwise to add an available renderer proc to a specific controller.
-
#
-
# Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
-
# include <tt>ActionController::Renderers::All</tt>, making all renderers
-
# available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
-
#
-
# Since <tt>ActionController::Metal</tt> controllers cannot render, the controller
-
# must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>,
-
# and <tt>ActionController::Renderers</tt>, and have at least one renderer.
-
#
-
# Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
-
# you may specify which renderers to include by passing the renderer name or names to
-
# +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer
-
# (+_render_with_renderer_json+) might look like:
-
#
-
# class MetalRenderingController < ActionController::Metal
-
# include AbstractController::Rendering
-
# include ActionController::Rendering
-
# include ActionController::Renderers
-
#
-
# use_renderers :json
-
#
-
# def show
-
# render json: record
-
# end
-
# end
-
#
-
# You must specify a +use_renderer+, else the +controller.renderer+ and
-
# +controller._renderers+ will be <tt>nil</tt>, and the action will fail.
-
1
def use_renderers(*args)
-
renderers = _renderers + args
-
self._renderers = renderers.freeze
-
end
-
1
alias use_renderer use_renderers
-
end
-
-
# Called by +render+ in <tt>AbstractController::Rendering</tt>
-
# which sets the return value as the +response_body+.
-
#
-
# If no renderer is found, +super+ returns control to
-
# <tt>ActionView::Rendering.render_to_body</tt>, if present.
-
1
def render_to_body(options)
-
_render_to_body_with_renderer(options) || super
-
end
-
-
1
def _render_to_body_with_renderer(options)
-
_renderers.each do |name|
-
if options.key?(name)
-
_process_options(options)
-
method_name = Renderers._render_with_renderer_method_name(name)
-
return send(method_name, options.delete(name), options)
-
end
-
end
-
nil
-
end
-
-
1
add :json do |json, options|
-
json = json.to_json(options) unless json.kind_of?(String)
-
-
if options[:callback].present?
-
if media_type.nil? || media_type == Mime[:json]
-
self.content_type = Mime[:js]
-
end
-
-
"/**/#{options[:callback]}(#{json})"
-
else
-
self.content_type = Mime[:json] if media_type.nil?
-
json
-
end
-
end
-
-
1
add :js do |js, options|
-
self.content_type = Mime[:js] if media_type.nil?
-
js.respond_to?(:to_js) ? js.to_js(options) : js
-
end
-
-
1
add :xml do |xml, options|
-
self.content_type = Mime[:xml] if media_type.nil?
-
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module Rendering
-
1
extend ActiveSupport::Concern
-
-
1
RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html]
-
-
1
module ClassMethods
-
# Documentation at ActionController::Renderer#render
-
1
delegate :render, to: :renderer
-
-
# Returns a renderer instance (inherited from ActionController::Renderer)
-
# for the controller.
-
1
attr_reader :renderer
-
-
1
def setup_renderer! # :nodoc:
-
2
@renderer = Renderer.for(self)
-
end
-
-
1
def inherited(klass)
-
1
klass.setup_renderer!
-
1
super
-
end
-
end
-
-
# Before processing, set the request formats in current controller formats.
-
1
def process_action(*) #:nodoc:
-
self.formats = request.formats.map(&:ref).compact
-
super
-
end
-
-
# Check for double render errors and set the content_type after rendering.
-
1
def render(*args) #:nodoc:
-
raise ::AbstractController::DoubleRenderError if response_body
-
super
-
end
-
-
# Overwrite render_to_string because body can now be set to a Rack body.
-
1
def render_to_string(*)
-
result = super
-
if result.respond_to?(:each)
-
string = +""
-
result.each { |r| string << r }
-
string
-
else
-
result
-
end
-
end
-
-
1
def render_to_body(options = {})
-
super || _render_in_priorities(options) || " "
-
end
-
-
1
private
-
1
def _process_variant(options)
-
if defined?(request) && !request.nil? && request.variant.present?
-
options[:variant] = request.variant
-
end
-
end
-
-
1
def _render_in_priorities(options)
-
RENDER_FORMATS_IN_PRIORITY.each do |format|
-
return options[format] if options.key?(format)
-
end
-
-
nil
-
end
-
-
1
def _set_html_content_type
-
self.content_type = Mime[:html].to_s
-
end
-
-
1
def _set_rendered_content_type(format)
-
if format && !response.media_type
-
self.content_type = format.to_s
-
end
-
end
-
-
1
def _set_vary_header
-
if self.headers["Vary"].blank? && request.should_apply_vary_header?
-
self.headers["Vary"] = "Accept"
-
end
-
end
-
-
# Normalize arguments by catching blocks and setting them on :update.
-
1
def _normalize_args(action = nil, options = {}, &blk)
-
options = super
-
options[:update] = blk if block_given?
-
options
-
end
-
-
# Normalize both text and status options.
-
1
def _normalize_options(options)
-
_normalize_text(options)
-
-
if options[:html]
-
options[:html] = ERB::Util.html_escape(options[:html])
-
end
-
-
if options[:status]
-
options[:status] = Rack::Utils.status_code(options[:status])
-
end
-
-
super
-
end
-
-
1
def _normalize_text(options)
-
RENDER_FORMATS_IN_PRIORITY.each do |format|
-
if options.key?(format) && options[format].respond_to?(:to_text)
-
options[format] = options[format].to_text
-
end
-
end
-
end
-
-
# Process controller specific options, as status, content-type and location.
-
1
def _process_options(options)
-
status, content_type, location = options.values_at(:status, :content_type, :location)
-
-
self.status = status if status
-
self.content_type = content_type if content_type
-
headers["Location"] = url_for(location) if location
-
-
super
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/session/abstract/id"
-
1
require "action_controller/metal/exceptions"
-
1
require "active_support/security_utils"
-
-
1
module ActionController #:nodoc:
-
1
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
-
end
-
-
1
class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
-
end
-
-
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
-
# by including a token in the rendered HTML for your application. This token is
-
# stored as a random string in the session, to which an attacker does not have
-
# access. When a request reaches your application, \Rails verifies the received
-
# token with the token in the session. All requests are checked except GET requests
-
# as these should be idempotent. Keep in mind that all session-oriented requests
-
# are CSRF protected by default, including JavaScript and HTML requests.
-
#
-
# Since HTML and JavaScript requests are typically made from the browser, we
-
# need to ensure to verify request authenticity for the web browser. We can
-
# use session-oriented authentication for these types of requests, by using
-
# the <tt>protect_from_forgery</tt> method in our controllers.
-
#
-
# GET requests are not protected since they don't have side effects like writing
-
# to the database and don't leak sensitive information. JavaScript requests are
-
# an exception: a third-party site can use a <script> tag to reference a JavaScript
-
# URL on your site. When your JavaScript response loads on their site, it executes.
-
# With carefully crafted JavaScript on their end, sensitive data in your JavaScript
-
# response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
-
# Ajax) requests are allowed to make requests for JavaScript responses.
-
#
-
# Subclasses of <tt>ActionController::Base</tt> are protected by default with the
-
# <tt>:exception</tt> strategy, which raises an
-
# <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
-
#
-
# APIs may want to disable this behavior since they are typically designed to be
-
# state-less: that is, the request API client handles the session instead of Rails.
-
# One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
-
# which allows unverified requests to be handled, but with an empty session:
-
#
-
# class ApplicationController < ActionController::Base
-
# protect_from_forgery with: :null_session
-
# end
-
#
-
# Note that API only applications don't include this module or a session middleware
-
# by default, and so don't require CSRF protection to be configured.
-
#
-
# The token parameter is named <tt>authenticity_token</tt> by default. The name and
-
# value of this token must be added to every layout that renders forms by including
-
# <tt>csrf_meta_tags</tt> in the HTML +head+.
-
#
-
# Learn more about CSRF attacks and securing your application in the
-
# {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
-
1
module RequestForgeryProtection
-
1
extend ActiveSupport::Concern
-
-
1
include AbstractController::Helpers
-
1
include AbstractController::Callbacks
-
-
1
included do
-
# Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
-
# sets it to <tt>:authenticity_token</tt> by default.
-
1
config_accessor :request_forgery_protection_token
-
1
self.request_forgery_protection_token ||= :authenticity_token
-
-
# Holds the class which implements the request forgery protection.
-
1
config_accessor :forgery_protection_strategy
-
1
self.forgery_protection_strategy = nil
-
-
# Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
-
1
config_accessor :allow_forgery_protection
-
1
self.allow_forgery_protection = true if allow_forgery_protection.nil?
-
-
# Controls whether a CSRF failure logs a warning. On by default.
-
1
config_accessor :log_warning_on_csrf_failure
-
1
self.log_warning_on_csrf_failure = true
-
-
# Controls whether the Origin header is checked in addition to the CSRF token.
-
1
config_accessor :forgery_protection_origin_check
-
1
self.forgery_protection_origin_check = false
-
-
# Controls whether form-action/method specific CSRF tokens are used.
-
1
config_accessor :per_form_csrf_tokens
-
1
self.per_form_csrf_tokens = false
-
-
# Controls whether forgery protection is enabled by default.
-
1
config_accessor :default_protect_from_forgery
-
1
self.default_protect_from_forgery = false
-
-
# Controls whether URL-safe CSRF tokens are generated.
-
1
config_accessor :urlsafe_csrf_tokens, instance_writer: false
-
1
self.urlsafe_csrf_tokens = false
-
-
1
helper_method :form_authenticity_token
-
1
helper_method :protect_against_forgery?
-
end
-
-
1
module ClassMethods
-
# Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
-
#
-
# class ApplicationController < ActionController::Base
-
# protect_from_forgery
-
# end
-
#
-
# class FooController < ApplicationController
-
# protect_from_forgery except: :index
-
# end
-
#
-
# You can disable forgery protection on controller by skipping the verification before_action:
-
#
-
# skip_before_action :verify_authenticity_token
-
#
-
# Valid Options:
-
#
-
# * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
-
# * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
-
# * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
-
# protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
-
# when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
-
#
-
# If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
-
# * <tt>:with</tt> - Set the method to handle unverified request.
-
#
-
# Valid unverified request handling methods are:
-
# * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
-
# * <tt>:reset_session</tt> - Resets the session.
-
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
-
1
def protect_from_forgery(options = {})
-
1
options = options.reverse_merge(prepend: false)
-
-
1
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
-
1
self.request_forgery_protection_token ||= :authenticity_token
-
1
before_action :verify_authenticity_token, options
-
1
append_after_action :verify_same_origin_request
-
end
-
-
# Turn off request forgery protection. This is a wrapper for:
-
#
-
# skip_before_action :verify_authenticity_token
-
#
-
# See +skip_before_action+ for allowed options.
-
1
def skip_forgery_protection(options = {})
-
skip_before_action :verify_authenticity_token, options
-
end
-
-
1
private
-
1
def protection_method_class(name)
-
1
ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
-
rescue NameError
-
raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
-
end
-
end
-
-
1
module ProtectionMethods
-
1
class NullSession
-
1
def initialize(controller)
-
@controller = controller
-
end
-
-
# This is the method that defines the application behavior when a request is found to be unverified.
-
1
def handle_unverified_request
-
request = @controller.request
-
request.session = NullSessionHash.new(request)
-
request.flash = nil
-
request.session_options = { skip: true }
-
request.cookie_jar = NullCookieJar.build(request, {})
-
end
-
-
1
private
-
1
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
-
1
def initialize(req)
-
super(nil, req)
-
@data = {}
-
@loaded = true
-
end
-
-
# no-op
-
1
def destroy; end
-
-
1
def exists?
-
true
-
end
-
end
-
-
1
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
-
1
def write(*)
-
# nothing
-
end
-
end
-
end
-
-
1
class ResetSession
-
1
def initialize(controller)
-
@controller = controller
-
end
-
-
1
def handle_unverified_request
-
@controller.reset_session
-
end
-
end
-
-
1
class Exception
-
1
def initialize(controller)
-
@controller = controller
-
end
-
-
1
def handle_unverified_request
-
raise ActionController::InvalidAuthenticityToken
-
end
-
end
-
end
-
-
1
private
-
# The actual before_action that is used to verify the CSRF token.
-
# Don't override this directly. Provide your own forgery protection
-
# strategy instead. If you override, you'll disable same-origin
-
# <tt><script></tt> verification.
-
#
-
# Lean on the protect_from_forgery declaration to mark which actions are
-
# due for same-origin request verification. If protect_from_forgery is
-
# enabled on an action, this before_action flags its after_action to
-
# verify that JavaScript responses are for XHR requests, ensuring they
-
# follow the browser's same-origin policy.
-
1
def verify_authenticity_token # :doc:
-
mark_for_same_origin_verification!
-
-
if !verified_request?
-
if logger && log_warning_on_csrf_failure
-
if valid_request_origin?
-
logger.warn "Can't verify CSRF token authenticity."
-
else
-
logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
-
end
-
end
-
handle_unverified_request
-
end
-
end
-
-
1
def handle_unverified_request # :doc:
-
forgery_protection_strategy.new(self).handle_unverified_request
-
end
-
-
#:nodoc:
-
1
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
-
"<script> tag on another site requested protected JavaScript. " \
-
"If you know what you're doing, go ahead and disable forgery " \
-
"protection on this action to permit cross-origin JavaScript embedding."
-
1
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
-
# :startdoc:
-
-
# If +verify_authenticity_token+ was run (indicating that we have
-
# forgery protection enabled for this request) then also verify that
-
# we aren't serving an unauthorized cross-origin response.
-
1
def verify_same_origin_request # :doc:
-
if marked_for_same_origin_verification? && non_xhr_javascript_response?
-
if logger && log_warning_on_csrf_failure
-
logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
-
end
-
raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
-
end
-
end
-
-
# GET requests are checked for cross-origin JavaScript after rendering.
-
1
def mark_for_same_origin_verification! # :doc:
-
@marked_for_same_origin_verification = request.get?
-
end
-
-
# If the +verify_authenticity_token+ before_action ran, verify that
-
# JavaScript responses are only served to same-origin GET requests.
-
1
def marked_for_same_origin_verification? # :doc:
-
@marked_for_same_origin_verification ||= false
-
end
-
-
# Check for cross-origin JavaScript responses.
-
1
def non_xhr_javascript_response? # :doc:
-
%r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
-
end
-
-
1
AUTHENTICITY_TOKEN_LENGTH = 32
-
-
# Returns true or false if a request is verified. Checks:
-
#
-
# * Is it a GET or HEAD request? GETs should be safe and idempotent
-
# * Does the form_authenticity_token match the given token value from the params?
-
# * Does the X-CSRF-Token header match the form_authenticity_token?
-
1
def verified_request? # :doc:
-
!protect_against_forgery? || request.get? || request.head? ||
-
(valid_request_origin? && any_authenticity_token_valid?)
-
end
-
-
# Checks if any of the authenticity tokens from the request are valid.
-
1
def any_authenticity_token_valid? # :doc:
-
request_authenticity_tokens.any? do |token|
-
valid_authenticity_token?(session, token)
-
end
-
end
-
-
# Possible authenticity tokens sent in the request.
-
1
def request_authenticity_tokens # :doc:
-
[form_authenticity_param, request.x_csrf_token]
-
end
-
-
# Sets the token value for the current session.
-
1
def form_authenticity_token(form_options: {})
-
masked_authenticity_token(session, form_options: form_options)
-
end
-
-
# Creates a masked version of the authenticity token that varies
-
# on each request. The masking is used to mitigate SSL attacks
-
# like BREACH.
-
1
def masked_authenticity_token(session, form_options: {}) # :doc:
-
action, method = form_options.values_at(:action, :method)
-
-
raw_token = if per_form_csrf_tokens && action && method
-
action_path = normalize_action_path(action)
-
per_form_csrf_token(session, action_path, method)
-
else
-
global_csrf_token(session)
-
end
-
-
mask_token(raw_token)
-
end
-
-
# Checks the client's masked token to see if it matches the
-
# session token. Essentially the inverse of
-
# +masked_authenticity_token+.
-
1
def valid_authenticity_token?(session, encoded_masked_token) # :doc:
-
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
-
return false
-
end
-
-
begin
-
masked_token = decode_csrf_token(encoded_masked_token)
-
rescue ArgumentError # encoded_masked_token is invalid Base64
-
return false
-
end
-
-
# See if it's actually a masked token or not. In order to
-
# deploy this code, we should be able to handle any unmasked
-
# tokens that we've issued without error.
-
-
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
-
# This is actually an unmasked token. This is expected if
-
# you have just upgraded to masked tokens, but should stop
-
# happening shortly after installing this gem.
-
compare_with_real_token masked_token, session
-
-
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
-
csrf_token = unmask_token(masked_token)
-
-
compare_with_global_token(csrf_token, session) ||
-
compare_with_real_token(csrf_token, session) ||
-
valid_per_form_csrf_token?(csrf_token, session)
-
else
-
false # Token is malformed.
-
end
-
end
-
-
1
def unmask_token(masked_token) # :doc:
-
# Split the token into the one-time pad and the encrypted
-
# value and decrypt it.
-
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
-
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
-
xor_byte_strings(one_time_pad, encrypted_csrf_token)
-
end
-
-
1
def mask_token(raw_token) # :doc:
-
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
-
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
-
masked_token = one_time_pad + encrypted_csrf_token
-
encode_csrf_token(masked_token)
-
end
-
-
1
def compare_with_real_token(token, session) # :doc:
-
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
-
end
-
-
1
def compare_with_global_token(token, session) # :doc:
-
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
-
end
-
-
1
def valid_per_form_csrf_token?(token, session) # :doc:
-
if per_form_csrf_tokens
-
correct_token = per_form_csrf_token(
-
session,
-
request.path.chomp("/"),
-
request.request_method
-
)
-
-
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
-
else
-
false
-
end
-
end
-
-
1
def real_csrf_token(session) # :doc:
-
session[:_csrf_token] ||= generate_csrf_token
-
decode_csrf_token(session[:_csrf_token])
-
end
-
-
1
def per_form_csrf_token(session, action_path, method) # :doc:
-
csrf_token_hmac(session, [action_path, method.downcase].join("#"))
-
end
-
-
1
GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
-
1
private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
-
-
1
def global_csrf_token(session) # :doc:
-
csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
-
end
-
-
1
def csrf_token_hmac(session, identifier) # :doc:
-
OpenSSL::HMAC.digest(
-
OpenSSL::Digest::SHA256.new,
-
real_csrf_token(session),
-
identifier
-
)
-
end
-
-
1
def xor_byte_strings(s1, s2) # :doc:
-
s2 = s2.dup
-
size = s1.bytesize
-
i = 0
-
while i < size
-
s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
-
i += 1
-
end
-
s2
-
end
-
-
# The form's authenticity parameter. Override to provide your own.
-
1
def form_authenticity_param # :doc:
-
params[request_forgery_protection_token]
-
end
-
-
# Checks if the controller allows forgery protection.
-
1
def protect_against_forgery? # :doc:
-
allow_forgery_protection
-
end
-
-
1
NULL_ORIGIN_MESSAGE = <<~MSG
-
The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
-
means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
-
refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
-
best solution is to change your referrer policy to something less strict like same-origin or strict-origin.
-
If you cannot change the referrer policy, you can disable origin checking with the
-
Rails.application.config.action_controller.forgery_protection_origin_check setting.
-
MSG
-
-
# Checks if the request originated from the same origin by looking at the
-
# Origin header.
-
1
def valid_request_origin? # :doc:
-
if forgery_protection_origin_check
-
# We accept blank origin headers because some user agents don't send it.
-
raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
-
request.origin.nil? || request.origin == request.base_url
-
else
-
true
-
end
-
end
-
-
1
def normalize_action_path(action_path) # :doc:
-
uri = URI.parse(action_path)
-
uri.path.chomp("/")
-
end
-
-
1
def generate_csrf_token # :nodoc:
-
if urlsafe_csrf_tokens
-
SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
-
else
-
SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
-
end
-
end
-
-
1
def encode_csrf_token(csrf_token) # :nodoc:
-
if urlsafe_csrf_tokens
-
Base64.urlsafe_encode64(csrf_token, padding: false)
-
else
-
Base64.strict_encode64(csrf_token)
-
end
-
end
-
-
1
def decode_csrf_token(encoded_csrf_token) # :nodoc:
-
if urlsafe_csrf_tokens
-
Base64.urlsafe_decode64(encoded_csrf_token)
-
else
-
begin
-
Base64.strict_decode64(encoded_csrf_token)
-
rescue ArgumentError
-
Base64.urlsafe_decode64(encoded_csrf_token)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController #:nodoc:
-
# This module is responsible for providing +rescue_from+ helpers
-
# to controllers and configuring when detailed exceptions must be
-
# shown.
-
1
module Rescue
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Rescuable
-
-
# Override this method if you want to customize when detailed
-
# exceptions must be shown. This method is only called when
-
# +consider_all_requests_local+ is +false+. By default, it returns
-
# +false+, but someone may set it to <tt>request.local?</tt> so local
-
# requests in production still show the detailed exception pages.
-
1
def show_detailed_exceptions?
-
false
-
end
-
-
1
private
-
1
def process_action(*)
-
super
-
rescue Exception => exception
-
request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
-
rescue_with_handler(exception) || raise
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/chunked"
-
-
1
module ActionController #:nodoc:
-
# Allows views to be streamed back to the client as they are rendered.
-
#
-
# By default, Rails renders views by first rendering the template
-
# and then the layout. The response is sent to the client after the whole
-
# template is rendered, all queries are made, and the layout is processed.
-
#
-
# Streaming inverts the rendering flow by rendering the layout first and
-
# streaming each part of the layout as they are processed. This allows the
-
# header of the HTML (which is usually in the layout) to be streamed back
-
# to client very quickly, allowing JavaScripts and stylesheets to be loaded
-
# earlier than usual.
-
#
-
# This approach was introduced in Rails 3.1 and is still improving. Several
-
# Rack middlewares may not work and you need to be careful when streaming.
-
# Those points are going to be addressed soon.
-
#
-
# In order to use streaming, you will need to use a Ruby version that
-
# supports fibers (fibers are supported since version 1.9.2 of the main
-
# Ruby implementation).
-
#
-
# Streaming can be added to a given template easily, all you need to do is
-
# to pass the :stream option.
-
#
-
# class PostsController
-
# def index
-
# @posts = Post.all
-
# render stream: true
-
# end
-
# end
-
#
-
# == When to use streaming
-
#
-
# Streaming may be considered to be overkill for lightweight actions like
-
# +new+ or +edit+. The real benefit of streaming is on expensive actions
-
# that, for example, do a lot of queries on the database.
-
#
-
# In such actions, you want to delay queries execution as much as you can.
-
# For example, imagine the following +dashboard+ action:
-
#
-
# def dashboard
-
# @posts = Post.all
-
# @pages = Page.all
-
# @articles = Article.all
-
# end
-
#
-
# Most of the queries here are happening in the controller. In order to benefit
-
# from streaming you would want to rewrite it as:
-
#
-
# def dashboard
-
# # Allow lazy execution of the queries
-
# @posts = Post.all
-
# @pages = Page.all
-
# @articles = Article.all
-
# render stream: true
-
# end
-
#
-
# Notice that :stream only works with templates. Rendering :json
-
# or :xml with :stream won't work.
-
#
-
# == Communication between layout and template
-
#
-
# When streaming, rendering happens top-down instead of inside-out.
-
# Rails starts with the layout, and the template is rendered later,
-
# when its +yield+ is reached.
-
#
-
# This means that, if your application currently relies on instance
-
# variables set in the template to be used in the layout, they won't
-
# work once you move to streaming. The proper way to communicate
-
# between layout and template, regardless of whether you use streaming
-
# or not, is by using +content_for+, +provide+ and +yield+.
-
#
-
# Take a simple example where the layout expects the template to tell
-
# which title to use:
-
#
-
# <html>
-
# <head><title><%= yield :title %></title></head>
-
# <body><%= yield %></body>
-
# </html>
-
#
-
# You would use +content_for+ in your template to specify the title:
-
#
-
# <%= content_for :title, "Main" %>
-
# Hello
-
#
-
# And the final result would be:
-
#
-
# <html>
-
# <head><title>Main</title></head>
-
# <body>Hello</body>
-
# </html>
-
#
-
# However, if +content_for+ is called several times, the final result
-
# would have all calls concatenated. For instance, if we have the following
-
# template:
-
#
-
# <%= content_for :title, "Main" %>
-
# Hello
-
# <%= content_for :title, " page" %>
-
#
-
# The final result would be:
-
#
-
# <html>
-
# <head><title>Main page</title></head>
-
# <body>Hello</body>
-
# </html>
-
#
-
# This means that, if you have <code>yield :title</code> in your layout
-
# and you want to use streaming, you would have to render the whole template
-
# (and eventually trigger all queries) before streaming the title and all
-
# assets, which kills the purpose of streaming. For this purpose, you can use
-
# a helper called +provide+ that does the same as +content_for+ but tells the
-
# layout to stop searching for other entries and continue rendering.
-
#
-
# For instance, the template above using +provide+ would be:
-
#
-
# <%= provide :title, "Main" %>
-
# Hello
-
# <%= content_for :title, " page" %>
-
#
-
# Giving:
-
#
-
# <html>
-
# <head><title>Main</title></head>
-
# <body>Hello</body>
-
# </html>
-
#
-
# That said, when streaming, you need to properly check your templates
-
# and choose when to use +provide+ and +content_for+.
-
#
-
# == Headers, cookies, session and flash
-
#
-
# When streaming, the HTTP headers are sent to the client right before
-
# it renders the first line. This means that, modifying headers, cookies,
-
# session or flash after the template starts rendering will not propagate
-
# to the client.
-
#
-
# == Middlewares
-
#
-
# Middlewares that need to manipulate the body won't work with streaming.
-
# You should disable those middlewares whenever streaming in development
-
# or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it
-
# needs to inject contents in the HTML body.
-
#
-
# Also <tt>Rack::Cache</tt> won't work with streaming as it does not support
-
# streaming bodies yet. Whenever streaming Cache-Control is automatically
-
# set to "no-cache".
-
#
-
# == Errors
-
#
-
# When it comes to streaming, exceptions get a bit more complicated. This
-
# happens because part of the template was already rendered and streamed to
-
# the client, making it impossible to render a whole exception page.
-
#
-
# Currently, when an exception happens in development or production, Rails
-
# will automatically stream to the client:
-
#
-
# "><script>window.location = "/500.html"</script></html>
-
#
-
# The first two characters (">) are required in case the exception happens
-
# while rendering attributes for a given tag. You can check the real cause
-
# for the exception in your logger.
-
#
-
# == Web server support
-
#
-
# Not all web servers support streaming out-of-the-box. You need to check
-
# the instructions for each of them.
-
#
-
# ==== Unicorn
-
#
-
# Unicorn supports streaming but it needs to be configured. For this, you
-
# need to create a config file as follow:
-
#
-
# # unicorn.config.rb
-
# listen 3000, tcp_nopush: false
-
#
-
# And use it on initialization:
-
#
-
# unicorn_rails --config-file unicorn.config.rb
-
#
-
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
-
# Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen
-
#
-
# If you are using Unicorn with NGINX, you may need to tweak NGINX.
-
# Streaming should work out of the box on Rainbows.
-
#
-
# ==== Passenger
-
#
-
# To be described.
-
#
-
1
module Streaming
-
1
extend ActiveSupport::Concern
-
-
1
private
-
# Set proper cache control and transfer encoding when streaming
-
1
def _process_options(options)
-
super
-
if options[:stream]
-
if request.version == "HTTP/1.0"
-
options.delete(:stream)
-
else
-
headers["Cache-Control"] ||= "no-cache"
-
headers["Transfer-Encoding"] = "chunked"
-
headers.delete("Content-Length")
-
end
-
end
-
end
-
-
# Call render_body if we are streaming instead of usual +render+.
-
1
def _render_template(options)
-
if options.delete(:stream)
-
Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
-
else
-
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module Testing
-
1
extend ActiveSupport::Concern
-
-
# Behavior specific to functional tests
-
1
module Functional # :nodoc:
-
1
def recycle!
-
@_url_options = nil
-
self.formats = nil
-
self.params = nil
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
-
# the <tt>_routes</tt> method. Otherwise, an exception will be raised.
-
#
-
# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
-
# URL options like the +host+. In order to do so, this module requires the host class
-
# to implement +env+ which needs to be Rack-compatible and +request+
-
# which is either an instance of +ActionDispatch::Request+ or an object
-
# that responds to the +host+, +optional_port+, +protocol+ and
-
# +symbolized_path_parameter+ methods.
-
#
-
# class RootUrl
-
# include ActionController::UrlFor
-
# include Rails.application.routes.url_helpers
-
#
-
# delegate :env, :request, to: :controller
-
#
-
# def initialize(controller)
-
# @controller = controller
-
# @url = root_path # named route from the application.
-
# end
-
# end
-
1
module UrlFor
-
1
extend ActiveSupport::Concern
-
-
1
include AbstractController::UrlFor
-
-
1
def url_options
-
@_url_options ||= {
-
host: request.host,
-
port: request.optional_port,
-
protocol: request.protocol,
-
_recall: request.path_parameters
-
}.merge!(super).freeze
-
-
if (same_origin = _routes.equal?(request.routes)) ||
-
(script_name = request.engine_script_name(_routes)) ||
-
(original_script_name = request.original_script_name)
-
-
options = @_url_options.dup
-
if original_script_name
-
options[:original_script_name] = original_script_name
-
else
-
if same_origin
-
options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup
-
else
-
options[:script_name] = script_name
-
end
-
end
-
options.freeze
-
else
-
@_url_options
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
# ActionController::Renderer allows you to render arbitrary templates
-
# without requirement of being in controller actions.
-
#
-
# You get a concrete renderer class by invoking ActionController::Base#renderer.
-
# For example:
-
#
-
# ApplicationController.renderer
-
#
-
# It allows you to call method #render directly.
-
#
-
# ApplicationController.renderer.render template: '...'
-
#
-
# You can use this shortcut in a controller, instead of the previous example:
-
#
-
# ApplicationController.render template: '...'
-
#
-
# #render allows you to use the same options that you can use when rendering in a controller.
-
# For example:
-
#
-
# FooController.render :action, locals: { ... }, assigns: { ... }
-
#
-
# The template will be rendered in a Rack environment which is accessible through
-
# ActionController::Renderer#env. You can set it up in two ways:
-
#
-
# * by changing renderer defaults, like
-
#
-
# ApplicationController.renderer.defaults # => hash with default Rack environment
-
#
-
# * by initializing an instance of renderer by passing it a custom environment.
-
#
-
# ApplicationController.renderer.new(method: 'post', https: true)
-
#
-
1
class Renderer
-
1
attr_reader :defaults, :controller
-
-
DEFAULTS = {
-
1
http_host: "example.org",
-
https: false,
-
method: "get",
-
script_name: "",
-
input: ""
-
}.freeze
-
-
# Create a new renderer instance for a specific controller class.
-
1
def self.for(controller, env = {}, defaults = DEFAULTS.dup)
-
2
new(controller, env, defaults)
-
end
-
-
# Create a new renderer for the same controller but with a new env.
-
1
def new(env = {})
-
self.class.new controller, env, defaults
-
end
-
-
# Create a new renderer for the same controller but with new defaults.
-
1
def with_defaults(defaults)
-
self.class.new controller, @env, self.defaults.merge(defaults)
-
end
-
-
# Accepts a custom Rack environment to render templates in.
-
# It will be merged with the default Rack environment defined by
-
# +ActionController::Renderer::DEFAULTS+.
-
1
def initialize(controller, env, defaults)
-
2
@controller = controller
-
2
@defaults = defaults
-
2
@env = normalize_keys defaults, env
-
end
-
-
# Render templates with any options from ActionController::Base#render_to_string.
-
#
-
# The primary options are:
-
# * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt> for details.
-
# * <tt>:file</tt> - Renders an explicit template file. Add <tt>:locals</tt> to pass in, if so desired.
-
# It shouldn’t be used directly with unsanitized user input due to lack of validation.
-
# * <tt>:inline</tt> - Renders an ERB template string.
-
# * <tt>:plain</tt> - Renders provided text and sets the content type as <tt>text/plain</tt>.
-
# * <tt>:html</tt> - Renders the provided HTML safe string, otherwise
-
# performs HTML escape on the string first. Sets the content type as <tt>text/html</tt>.
-
# * <tt>:json</tt> - Renders the provided hash or object in JSON. You don't
-
# need to call <tt>.to_json</tt> on the object you want to render.
-
# * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>.
-
#
-
# If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
-
#
-
# If an object responding to +render_in+ is passed, +render_in+ is called on the object,
-
# passing in the current view context.
-
#
-
# Otherwise, a partial is rendered using the second parameter as the locals hash.
-
1
def render(*args)
-
raise "missing controller" unless controller
-
-
request = ActionDispatch::Request.new @env
-
request.routes = controller._routes
-
-
instance = controller.new
-
instance.set_request! request
-
instance.set_response! controller.make_response!(request)
-
instance.render_to_string(*args)
-
end
-
1
alias_method :render_to_string, :render # :nodoc:
-
-
1
private
-
1
def normalize_keys(defaults, env)
-
2
new_env = {}
-
2
env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
-
-
2
defaults.each_pair do |k, v|
-
10
key = rack_key_for(k)
-
10
new_env[key] = rack_value_for(k, v) unless new_env.key?(key)
-
end
-
-
2
new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
-
2
new_env
-
end
-
-
RACK_KEY_TRANSLATION = {
-
1
http_host: "HTTP_HOST",
-
https: "HTTPS",
-
method: "REQUEST_METHOD",
-
script_name: "SCRIPT_NAME",
-
input: "rack.input"
-
}
-
-
1
def rack_key_for(key)
-
10
RACK_KEY_TRANSLATION[key] || key.to_s
-
end
-
-
1
def rack_value_for(key, value)
-
10
case key
-
when :https
-
2
value ? "on" : "off"
-
when :method
-
2
-value.upcase
-
else
-
6
value
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module TemplateAssertions # :nodoc:
-
1
def assert_template(options = {}, message = nil)
-
raise NoMethodError,
-
"assert_template has been extracted to a gem. To continue using it,
-
add `gem 'rails-controller-testing'` to your Gemfile."
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/session/abstract/id"
-
1
require "active_support/core_ext/hash/conversions"
-
1
require "active_support/core_ext/object/to_query"
-
1
require "active_support/core_ext/module/anonymous"
-
1
require "active_support/core_ext/module/redefine_method"
-
1
require "active_support/core_ext/hash/keys"
-
1
require "active_support/testing/constant_lookup"
-
1
require "action_controller/template_assertions"
-
1
require "rails-dom-testing"
-
-
1
module ActionController
-
1
class Metal
-
1
include Testing::Functional
-
end
-
-
1
module Live
-
# Disable controller / rendering threads in tests. User tests can access
-
# the database on the main thread, so they could open a txn, then the
-
# controller thread will open a new connection and try to access data
-
# that's only visible to the main thread's txn. This is the problem in #23483.
-
1
silence_redefinition_of_method :new_controller_thread
-
1
def new_controller_thread # :nodoc:
-
yield
-
end
-
end
-
-
# ActionController::TestCase will be deprecated and moved to a gem in the future.
-
# Please use ActionDispatch::IntegrationTest going forward.
-
1
class TestRequest < ActionDispatch::TestRequest #:nodoc:
-
1
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
-
1
DEFAULT_ENV.delete "PATH_INFO"
-
-
1
def self.new_session
-
TestSession.new
-
end
-
-
1
attr_reader :controller_class
-
-
# Create a new test request with default `env` values.
-
1
def self.create(controller_class)
-
env = {}
-
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
-
env["rack.request.cookie_hash"] = {}.with_indifferent_access
-
new(default_env.merge(env), new_session, controller_class)
-
end
-
-
1
def self.default_env
-
DEFAULT_ENV
-
end
-
1
private_class_method :default_env
-
-
1
def initialize(env, session, controller_class)
-
super(env)
-
-
self.session = session
-
self.session_options = TestSession::DEFAULT_OPTIONS.dup
-
@controller_class = controller_class
-
@custom_param_parsers = {
-
xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
-
}
-
end
-
-
1
def query_string=(string)
-
set_header Rack::QUERY_STRING, string
-
end
-
-
1
def content_type=(type)
-
set_header "CONTENT_TYPE", type
-
end
-
-
1
def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
-
non_path_parameters = {}
-
path_parameters = {}
-
-
parameters.each do |key, value|
-
if query_string_keys.include?(key)
-
non_path_parameters[key] = value
-
else
-
if value.is_a?(Array)
-
value = value.map(&:to_param)
-
else
-
value = value.to_param
-
end
-
-
path_parameters[key.to_sym] = value
-
end
-
end
-
-
if get?
-
if query_string.blank?
-
self.query_string = non_path_parameters.to_query
-
end
-
else
-
if ENCODER.should_multipart?(non_path_parameters)
-
self.content_type = ENCODER.content_type
-
data = ENCODER.build_multipart non_path_parameters
-
else
-
fetch_header("CONTENT_TYPE") do |k|
-
set_header k, "application/x-www-form-urlencoded"
-
end
-
-
case content_mime_type.to_sym
-
when nil
-
raise "Unknown Content-Type: #{content_type}"
-
when :json
-
data = ActiveSupport::JSON.encode(non_path_parameters)
-
when :xml
-
data = non_path_parameters.to_xml
-
when :url_encoded_form
-
data = non_path_parameters.to_query
-
else
-
@custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
-
data = non_path_parameters.to_query
-
end
-
end
-
-
data_stream = StringIO.new(data)
-
set_header "CONTENT_LENGTH", data_stream.length.to_s
-
set_header "rack.input", data_stream
-
end
-
-
fetch_header("PATH_INFO") do |k|
-
set_header k, generated_path
-
end
-
path_parameters[:controller] = controller_path
-
path_parameters[:action] = action
-
-
self.path_parameters = path_parameters
-
end
-
-
1
ENCODER = Class.new do
-
1
include Rack::Test::Utils
-
-
1
def should_multipart?(params)
-
# FIXME: lifted from Rack-Test. We should push this separation upstream.
-
multipart = false
-
query = lambda { |value|
-
case value
-
when Array
-
value.each(&query)
-
when Hash
-
value.values.each(&query)
-
when Rack::Test::UploadedFile
-
multipart = true
-
end
-
}
-
params.values.each(&query)
-
multipart
-
end
-
-
1
public :build_multipart
-
-
1
def content_type
-
"multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}"
-
end
-
end.new
-
-
1
private
-
1
def params_parsers
-
super.merge @custom_param_parsers
-
end
-
end
-
-
1
class LiveTestResponse < Live::Response
-
# Was the response successful?
-
1
alias_method :success?, :successful?
-
-
# Was the URL not found?
-
1
alias_method :missing?, :not_found?
-
-
# Was there a server-side error?
-
1
alias_method :error?, :server_error?
-
end
-
-
# Methods #destroy and #load! are overridden to avoid calling methods on the
-
# @store object, which does not exist for the TestSession class.
-
1
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
-
1
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
-
-
1
def initialize(session = {})
-
super(nil, nil)
-
@id = Rack::Session::SessionId.new(SecureRandom.hex(16))
-
@data = stringify_keys(session)
-
@loaded = true
-
end
-
-
1
def exists?
-
true
-
end
-
-
1
def keys
-
@data.keys
-
end
-
-
1
def values
-
@data.values
-
end
-
-
1
def destroy
-
clear
-
end
-
-
1
def dig(*keys)
-
keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
-
@data.dig(*keys)
-
end
-
-
1
def fetch(key, *args, &block)
-
@data.fetch(key.to_s, *args, &block)
-
end
-
-
1
private
-
1
def load!
-
@id
-
end
-
end
-
-
# Superclass for ActionController functional tests. Functional tests allow you to
-
# test a single controller action per test method.
-
#
-
# == Use integration style controller tests over functional style controller tests.
-
#
-
# Rails discourages the use of functional tests in favor of integration tests
-
# (use ActionDispatch::IntegrationTest).
-
#
-
# New Rails applications no longer generate functional style controller tests and they should
-
# only be used for backward compatibility. Integration style controller tests perform actual
-
# requests, whereas functional style controller tests merely simulate a request. Besides,
-
# integration tests are as fast as functional tests and provide lot of helpers such as +as+,
-
# +parsed_body+ for effective testing of controller actions including even API endpoints.
-
#
-
# == Basic example
-
#
-
# Functional tests are written as follows:
-
# 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate
-
# an HTTP request.
-
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
-
# the controller's HTTP response, the database contents, etc.
-
#
-
# For example:
-
#
-
# class BooksControllerTest < ActionController::TestCase
-
# def test_create
-
# # Simulate a POST response with the given HTTP parameters.
-
# post(:create, params: { book: { title: "Love Hina" }})
-
#
-
# # Asserts that the controller tried to redirect us to
-
# # the created book's URI.
-
# assert_response :found
-
#
-
# # Asserts that the controller really put the book in the database.
-
# assert_not_nil Book.find_by(title: "Love Hina")
-
# end
-
# end
-
#
-
# You can also send a real document in the simulated HTTP request.
-
#
-
# def test_create
-
# json = {book: { title: "Love Hina" }}.to_json
-
# post :create, body: json
-
# end
-
#
-
# == Special instance variables
-
#
-
# ActionController::TestCase will also automatically provide the following instance
-
# variables for use in the tests:
-
#
-
# <b>@controller</b>::
-
# The controller instance that will be tested.
-
# <b>@request</b>::
-
# An ActionController::TestRequest, representing the current HTTP
-
# request. You can modify this object before sending the HTTP request. For example,
-
# you might want to set some session properties before sending a GET request.
-
# <b>@response</b>::
-
# An ActionDispatch::TestResponse object, representing the response
-
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
-
# after calling +post+. If the various assert methods are not sufficient, then you
-
# may use this object to inspect the HTTP response in detail.
-
#
-
# == Controller is automatically inferred
-
#
-
# ActionController::TestCase will automatically infer the controller under test
-
# from the test class name. If the controller cannot be inferred from the test
-
# class name, you can explicitly set it with +tests+.
-
#
-
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
-
# tests WidgetController
-
# end
-
#
-
# == \Testing controller internals
-
#
-
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
-
# can be used against. These collections are:
-
#
-
# * session: Objects being saved in the session.
-
# * flash: The flash objects currently in the session.
-
# * cookies: \Cookies being sent to the user on this request.
-
#
-
# These collections can be used just like any other hash:
-
#
-
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
-
# assert flash.empty? # makes sure that there's nothing in the flash
-
#
-
# On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>.
-
#
-
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
-
# action call which can then be asserted against.
-
#
-
# == Manipulating session and cookie variables
-
#
-
# Sometimes you need to set up the session and cookie variables for a test.
-
# To do this just assign a value to the session or cookie collection:
-
#
-
# session[:key] = "value"
-
# cookies[:key] = "value"
-
#
-
# To clear the cookies for a test just clear the cookie collection:
-
#
-
# cookies.clear
-
#
-
# == \Testing named routes
-
#
-
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
-
#
-
# assert_redirected_to page_url(title: 'foo')
-
1
class TestCase < ActiveSupport::TestCase
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
1
include ActionDispatch::TestProcess
-
1
include ActiveSupport::Testing::ConstantLookup
-
1
include Rails::Dom::Testing::Assertions
-
-
1
attr_reader :response, :request
-
-
1
module ClassMethods
-
# Sets the controller class name. Useful if the name can't be inferred from test class.
-
# Normalizes +controller_class+ before using.
-
#
-
# tests WidgetController
-
# tests :widget
-
# tests 'widget'
-
1
def tests(controller_class)
-
case controller_class
-
when String, Symbol
-
self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize
-
when Class
-
self.controller_class = controller_class
-
else
-
raise ArgumentError, "controller class must be a String, Symbol, or Class"
-
end
-
end
-
-
1
def controller_class=(new_class)
-
self._controller_class = new_class
-
end
-
-
1
def controller_class
-
if current_controller_class = _controller_class
-
current_controller_class
-
else
-
self.controller_class = determine_default_controller_class(name)
-
end
-
end
-
-
1
def determine_default_controller_class(name)
-
determine_constant_from_test_name(name) do |constant|
-
Class === constant && constant < ActionController::Metal
-
end
-
end
-
end
-
-
# Simulate a GET request with the given parameters.
-
#
-
# - +action+: The controller action to call.
-
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
-
# - +body+: The request body with a string that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
-
# - +session+: A hash of parameters to store in the session. This may be +nil+.
-
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
-
#
-
# You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
-
# +post+, +patch+, +put+, +delete+, and +head+.
-
# Example sending parameters, session and setting a flash message:
-
#
-
# get :show,
-
# params: { id: 7 },
-
# session: { user_id: 1 },
-
# flash: { notice: 'This is flash message' }
-
#
-
# Note that the request method is not verified. The different methods are
-
# available to make the tests more expressive.
-
1
def get(action, **args)
-
res = process(action, method: "GET", **args)
-
cookies.update res.cookies
-
res
-
end
-
-
# Simulate a POST request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def post(action, **args)
-
process(action, method: "POST", **args)
-
end
-
-
# Simulate a PATCH request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def patch(action, **args)
-
process(action, method: "PATCH", **args)
-
end
-
-
# Simulate a PUT request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def put(action, **args)
-
process(action, method: "PUT", **args)
-
end
-
-
# Simulate a DELETE request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def delete(action, **args)
-
process(action, method: "DELETE", **args)
-
end
-
-
# Simulate a HEAD request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def head(action, **args)
-
process(action, method: "HEAD", **args)
-
end
-
-
# Simulate an HTTP request to +action+ by specifying request method,
-
# parameters and set/volley the response.
-
#
-
# - +action+: The controller action to call.
-
# - +method+: Request method used to send the HTTP request. Possible values
-
# are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. Can be a symbol.
-
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
-
# - +body+: The request body with a string that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
-
# - +session+: A hash of parameters to store in the session. This may be +nil+.
-
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
-
# - +format+: Request format. Defaults to +nil+. Can be string or symbol.
-
# - +as+: Content type. Defaults to +nil+. Must be a symbol that corresponds
-
# to a mime type.
-
#
-
# Example calling +create+ action and sending two params:
-
#
-
# process :create,
-
# method: 'POST',
-
# params: {
-
# user: { name: 'Gaurish Sharma', email: 'user@example.com' }
-
# },
-
# session: { user_id: 1 },
-
# flash: { notice: 'This is flash message' }
-
#
-
# To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests
-
# prefer using #get, #post, #patch, #put, #delete and #head methods
-
# respectively which will make tests more expressive.
-
#
-
# Note that the request method is not verified.
-
1
def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
-
check_required_ivars
-
-
action = +action.to_s
-
http_method = method.to_s.upcase
-
-
@html_document = nil
-
-
cookies.update(@request.cookies)
-
cookies.update_cookies_from_jar
-
@request.set_header "HTTP_COOKIE", cookies.to_header
-
@request.delete_header "action_dispatch.cookies"
-
-
@request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class
-
@response = build_response @response_klass
-
@response.request = @request
-
@controller.recycle!
-
-
if body
-
@request.set_header "RAW_POST_DATA", body
-
end
-
-
@request.set_header "REQUEST_METHOD", http_method
-
-
if as
-
@request.content_type = Mime[as].to_s
-
format ||= as
-
end
-
-
parameters = (params || {}).symbolize_keys
-
-
if format
-
parameters[:format] = format
-
end
-
-
setup_request(controller_class_name, action, parameters, session, flash, xhr)
-
process_controller_response(action, cookies, xhr)
-
end
-
-
1
def controller_class_name
-
@controller.class.anonymous? ? "anonymous" : @controller.class.controller_path
-
end
-
-
1
def generated_path(generated_extras)
-
generated_extras[0]
-
end
-
-
1
def query_parameter_names(generated_extras)
-
generated_extras[1] + [:controller, :action]
-
end
-
-
1
def setup_controller_request_and_response
-
@controller = nil unless defined? @controller
-
-
@response_klass = ActionDispatch::TestResponse
-
-
if klass = self.class.controller_class
-
if klass < ActionController::Live
-
@response_klass = LiveTestResponse
-
end
-
unless @controller
-
begin
-
@controller = klass.new
-
rescue
-
warn "could not construct controller #{klass}" if $VERBOSE
-
end
-
end
-
end
-
-
@request = TestRequest.create(@controller.class)
-
@response = build_response @response_klass
-
@response.request = @request
-
-
if @controller
-
@controller.request = @request
-
@controller.params = {}
-
end
-
end
-
-
1
def build_response(klass)
-
klass.create
-
end
-
-
1
included do
-
1
include ActionController::TemplateAssertions
-
1
include ActionDispatch::Assertions
-
1
class_attribute :_controller_class
-
1
setup :setup_controller_request_and_response
-
1
ActiveSupport.run_load_hooks(:action_controller_test_case, self)
-
end
-
-
1
private
-
1
def setup_request(controller_class_name, action, parameters, session, flash, xhr)
-
generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
-
generated_path = generated_path(generated_extras)
-
query_string_keys = query_parameter_names(generated_extras)
-
-
@request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
-
-
@request.session.update(session) if session
-
@request.flash.update(flash || {})
-
-
if xhr
-
@request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
-
@request.fetch_header("HTTP_ACCEPT") do |k|
-
@request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
-
end
-
end
-
-
@request.fetch_header("SCRIPT_NAME") do |k|
-
@request.set_header k, @controller.config.relative_url_root
-
end
-
end
-
-
1
def process_controller_response(action, cookies, xhr)
-
begin
-
@controller.recycle!
-
@controller.dispatch(action, @request, @response)
-
ensure
-
@request = @controller.request
-
@response = @controller.response
-
-
if @request.have_cookie_jar?
-
unless @request.cookie_jar.committed?
-
@request.cookie_jar.write(@response)
-
cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
-
end
-
end
-
@response.prepare!
-
-
if flash_value = @request.flash.to_session_value
-
@request.session["flash"] = flash_value
-
else
-
@request.session.delete("flash")
-
end
-
-
if xhr
-
@request.delete_header "HTTP_X_REQUESTED_WITH"
-
@request.delete_header "HTTP_ACCEPT"
-
end
-
@request.query_string = ""
-
-
@response.sent!
-
end
-
-
@response
-
end
-
-
1
def scrub_env!(env)
-
env.delete_if do |k, _|
-
k.start_with?("rack.request", "action_dispatch.request", "action_dispatch.rescue")
-
end
-
env["rack.input"] = StringIO.new
-
env.delete "CONTENT_LENGTH"
-
env.delete "RAW_POST_DATA"
-
env
-
end
-
-
1
def document_root_element
-
html_document.root
-
end
-
-
1
def check_required_ivars
-
# Sanity check for required instance variables so we can give an
-
# understandable error message.
-
[:@routes, :@controller, :@request, :@response].each do |iv_name|
-
if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
-
raise "#{iv_name} is nil: make sure you set it in your test's setup method."
-
end
-
end
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
1
module Http
-
1
module FilterRedirect
-
1
FILTERED = "[FILTERED]" # :nodoc:
-
-
1
def filtered_location # :nodoc:
-
if location_filter_match?
-
FILTERED
-
else
-
location
-
end
-
end
-
-
1
private
-
1
def location_filters
-
if request
-
request.get_header("action_dispatch.redirect_filter") || []
-
else
-
[]
-
end
-
end
-
-
1
def location_filter_match?
-
location_filters.any? do |filter|
-
if String === filter
-
location.include?(filter)
-
elsif Regexp === filter
-
location.match?(filter)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/attribute_accessors"
-
1
require "action_dispatch/http/filter_redirect"
-
1
require "action_dispatch/http/cache"
-
1
require "monitor"
-
-
1
module ActionDispatch # :nodoc:
-
# Represents an HTTP response generated by a controller action. Use it to
-
# retrieve the current state of the response, or customize the response. It can
-
# either represent a real HTTP response (i.e. one that is meant to be sent
-
# back to the web browser) or a TestResponse (i.e. one that is generated
-
# from integration tests).
-
#
-
# \Response is mostly a Ruby on \Rails framework implementation detail, and
-
# should never be used directly in controllers. Controllers should use the
-
# methods defined in ActionController::Base instead. For example, if you want
-
# to set the HTTP response's content MIME type, then use
-
# ActionControllerBase#headers instead of Response#headers.
-
#
-
# Nevertheless, integration tests may want to inspect controller responses in
-
# more detail, and that's when \Response can be useful for application
-
# developers. Integration test methods such as
-
# ActionDispatch::Integration::Session#get and
-
# ActionDispatch::Integration::Session#post return objects of type
-
# TestResponse (which are of course also of type \Response).
-
#
-
# For example, the following demo integration test prints the body of the
-
# controller response to the console:
-
#
-
# class DemoControllerTest < ActionDispatch::IntegrationTest
-
# def test_print_root_path_to_console
-
# get('/')
-
# puts response.body
-
# end
-
# end
-
1
class Response
-
1
class Header < DelegateClass(Hash) # :nodoc:
-
1
def initialize(response, header)
-
@response = response
-
super(header)
-
end
-
-
1
def []=(k, v)
-
if @response.sending? || @response.sent?
-
raise ActionDispatch::IllegalStateError, "header already sent"
-
end
-
-
super
-
end
-
-
1
def merge(other)
-
self.class.new @response, __getobj__.merge(other)
-
end
-
-
1
def to_hash
-
__getobj__.dup
-
end
-
end
-
-
# The request that the response is responding to.
-
1
attr_accessor :request
-
-
# The HTTP status code.
-
1
attr_reader :status
-
-
# Get headers for this response.
-
1
attr_reader :header
-
-
1
alias_method :headers, :header
-
-
1
delegate :[], :[]=, to: :@header
-
-
1
def each(&block)
-
sending!
-
x = @stream.each(&block)
-
sent!
-
x
-
end
-
-
1
CONTENT_TYPE = "Content-Type"
-
1
SET_COOKIE = "Set-Cookie"
-
1
LOCATION = "Location"
-
1
NO_CONTENT_CODES = [100, 101, 102, 103, 204, 205, 304]
-
-
1
cattr_accessor :default_charset, default: "utf-8"
-
1
cattr_accessor :default_headers
-
-
1
def self.return_only_media_type_on_content_type=(*)
-
ActiveSupport::Deprecation.warn(
-
".return_only_media_type_on_content_type= is dreprecated with no replacement and will be removed in 6.2."
-
)
-
end
-
-
1
def self.return_only_media_type_on_content_type
-
ActiveSupport::Deprecation.warn(
-
".return_only_media_type_on_content_type is dreprecated with no replacement and will be removed in 6.2."
-
)
-
end
-
-
1
include Rack::Response::Helpers
-
# Aliasing these off because AD::Http::Cache::Response defines them.
-
1
alias :_cache_control :cache_control
-
1
alias :_cache_control= :cache_control=
-
-
1
include ActionDispatch::Http::FilterRedirect
-
1
include ActionDispatch::Http::Cache::Response
-
1
include MonitorMixin
-
-
1
class Buffer # :nodoc:
-
1
def initialize(response, buf)
-
@response = response
-
@buf = buf
-
@closed = false
-
@str_body = nil
-
end
-
-
1
def body
-
@str_body ||= begin
-
buf = +""
-
each { |chunk| buf << chunk }
-
buf
-
end
-
end
-
-
1
def write(string)
-
raise IOError, "closed stream" if closed?
-
-
@str_body = nil
-
@response.commit!
-
@buf.push string
-
end
-
-
1
def each(&block)
-
if @str_body
-
return enum_for(:each) unless block_given?
-
-
yield @str_body
-
else
-
each_chunk(&block)
-
end
-
end
-
-
1
def abort
-
end
-
-
1
def close
-
@response.commit!
-
@closed = true
-
end
-
-
1
def closed?
-
@closed
-
end
-
-
1
private
-
1
def each_chunk(&block)
-
@buf.each(&block)
-
end
-
end
-
-
1
def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
-
header = merge_default_headers(header, default_headers)
-
new status, header, body
-
end
-
-
1
def self.merge_default_headers(original, default)
-
default.respond_to?(:merge) ? default.merge(original) : original
-
end
-
-
# The underlying body, as a streamable object.
-
1
attr_reader :stream
-
-
1
def initialize(status = 200, header = {}, body = [])
-
super()
-
-
@header = Header.new(self, header)
-
-
self.body, self.status = body, status
-
-
@cv = new_cond
-
@committed = false
-
@sending = false
-
@sent = false
-
-
prepare_cache_control!
-
-
yield self if block_given?
-
end
-
-
1
def has_header?(key); headers.key? key; end
-
1
def get_header(key); headers[key]; end
-
1
def set_header(key, v); headers[key] = v; end
-
1
def delete_header(key); headers.delete key; end
-
-
1
def await_commit
-
synchronize do
-
@cv.wait_until { @committed }
-
end
-
end
-
-
1
def await_sent
-
synchronize { @cv.wait_until { @sent } }
-
end
-
-
1
def commit!
-
synchronize do
-
before_committed
-
@committed = true
-
@cv.broadcast
-
end
-
end
-
-
1
def sending!
-
synchronize do
-
before_sending
-
@sending = true
-
@cv.broadcast
-
end
-
end
-
-
1
def sent!
-
synchronize do
-
@sent = true
-
@cv.broadcast
-
end
-
end
-
-
1
def sending?; synchronize { @sending }; end
-
1
def committed?; synchronize { @committed }; end
-
1
def sent?; synchronize { @sent }; end
-
-
# Sets the HTTP status code.
-
1
def status=(status)
-
@status = Rack::Utils.status_code(status)
-
end
-
-
# Sets the HTTP response's content MIME type. For example, in the controller
-
# you could write this:
-
#
-
# response.content_type = "text/plain"
-
#
-
# If a character set has been defined for this response (see charset=) then
-
# the character set information will also be included in the content type
-
# information.
-
1
def content_type=(content_type)
-
return unless content_type
-
new_header_info = parse_content_type(content_type.to_s)
-
prev_header_info = parsed_content_type_header
-
charset = new_header_info.charset || prev_header_info.charset
-
charset ||= self.class.default_charset unless prev_header_info.mime_type
-
set_content_type new_header_info.mime_type, charset
-
end
-
-
# Content type of response.
-
1
def content_type
-
super.presence
-
end
-
-
# Media type of response.
-
1
def media_type
-
parsed_content_type_header.mime_type
-
end
-
-
1
def sending_file=(v)
-
if true == v
-
self.charset = false
-
end
-
end
-
-
# Sets the HTTP character set. In case of +nil+ parameter
-
# it sets the charset to +default_charset+.
-
#
-
# response.charset = 'utf-16' # => 'utf-16'
-
# response.charset = nil # => 'utf-8'
-
1
def charset=(charset)
-
content_type = parsed_content_type_header.mime_type
-
if false == charset
-
set_content_type content_type, nil
-
else
-
set_content_type content_type, charset || self.class.default_charset
-
end
-
end
-
-
# The charset of the response. HTML wants to know the encoding of the
-
# content you're giving them, so we need to send that along.
-
1
def charset
-
header_info = parsed_content_type_header
-
header_info.charset || self.class.default_charset
-
end
-
-
# The response code of the request.
-
1
def response_code
-
@status
-
end
-
-
# Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
-
1
def code
-
@status.to_s
-
end
-
-
# Returns the corresponding message for the current HTTP status code:
-
#
-
# response.status = 200
-
# response.message # => "OK"
-
#
-
# response.status = 404
-
# response.message # => "Not Found"
-
#
-
1
def message
-
Rack::Utils::HTTP_STATUS_CODES[@status]
-
end
-
1
alias_method :status_message, :message
-
-
# Returns the content of the response as a string. This contains the contents
-
# of any calls to <tt>render</tt>.
-
1
def body
-
@stream.body
-
end
-
-
1
def write(string)
-
@stream.write string
-
end
-
-
# Allows you to manually set or override the response body.
-
1
def body=(body)
-
if body.respond_to?(:to_path)
-
@stream = body
-
else
-
synchronize do
-
@stream = build_buffer self, munge_body_object(body)
-
end
-
end
-
end
-
-
# Avoid having to pass an open file handle as the response body.
-
# Rack::Sendfile will usually intercept the response and uses
-
# the path directly, so there is no reason to open the file.
-
1
class FileBody #:nodoc:
-
1
attr_reader :to_path
-
-
1
def initialize(path)
-
@to_path = path
-
end
-
-
1
def body
-
File.binread(to_path)
-
end
-
-
# Stream the file's contents if Rack::Sendfile isn't present.
-
1
def each
-
File.open(to_path, "rb") do |file|
-
while chunk = file.read(16384)
-
yield chunk
-
end
-
end
-
end
-
end
-
-
# Send the file stored at +path+ as the response body.
-
1
def send_file(path)
-
commit!
-
@stream = FileBody.new(path)
-
end
-
-
1
def reset_body!
-
@stream = build_buffer(self, [])
-
end
-
-
1
def body_parts
-
parts = []
-
@stream.each { |x| parts << x }
-
parts
-
end
-
-
# The location header we'll be responding with.
-
1
alias_method :redirect_url, :location
-
-
1
def close
-
stream.close if stream.respond_to?(:close)
-
end
-
-
1
def abort
-
if stream.respond_to?(:abort)
-
stream.abort
-
elsif stream.respond_to?(:close)
-
# `stream.close` should really be reserved for a close from the
-
# other direction, but we must fall back to it for
-
# compatibility.
-
stream.close
-
end
-
end
-
-
# Turns the Response into a Rack-compatible array of the status, headers,
-
# and body. Allows explicit splatting:
-
#
-
# status, headers, body = *response
-
1
def to_a
-
commit!
-
rack_response @status, @header.to_hash
-
end
-
1
alias prepare! to_a
-
-
# Returns the response cookies, converted to a Hash of (name => value) pairs
-
#
-
# assert_equal 'AuthorOfNewPage', r.cookies['author']
-
1
def cookies
-
cookies = {}
-
if header = get_header(SET_COOKIE)
-
header = header.split("\n") if header.respond_to?(:to_str)
-
header.each do |cookie|
-
if pair = cookie.split(";").first
-
key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
-
cookies[key] = value
-
end
-
end
-
end
-
cookies
-
end
-
-
1
private
-
1
ContentTypeHeader = Struct.new :mime_type, :charset
-
1
NullContentTypeHeader = ContentTypeHeader.new nil, nil
-
-
1
CONTENT_TYPE_PARSER = /
-
\A
-
(?<mime_type>[^;\s]+\s*(?:;\s*(?:(?!charset)[^;\s])+)*)?
-
(?:;\s*charset=(?<quote>"?)(?<charset>[^;\s]+)\k<quote>)?
-
/x # :nodoc:
-
-
1
def parse_content_type(content_type)
-
if content_type && match = CONTENT_TYPE_PARSER.match(content_type)
-
ContentTypeHeader.new(match[:mime_type], match[:charset])
-
else
-
NullContentTypeHeader
-
end
-
end
-
-
# Small internal convenience method to get the parsed version of the current
-
# content type header.
-
1
def parsed_content_type_header
-
parse_content_type(get_header(CONTENT_TYPE))
-
end
-
-
1
def set_content_type(content_type, charset)
-
type = content_type || ""
-
type = "#{type}; charset=#{charset.to_s.downcase}" if charset
-
set_header CONTENT_TYPE, type
-
end
-
-
1
def before_committed
-
return if committed?
-
assign_default_content_type_and_charset!
-
merge_and_normalize_cache_control!(@cache_control)
-
handle_conditional_get!
-
handle_no_content!
-
end
-
-
1
def before_sending
-
# Normally we've already committed by now, but it's possible
-
# (e.g., if the controller action tries to read back its own
-
# response) to get here before that. In that case, we must force
-
# an "early" commit: we're about to freeze the headers, so this is
-
# our last chance.
-
commit! unless committed?
-
-
headers.freeze
-
request.commit_cookie_jar! unless committed?
-
end
-
-
1
def build_buffer(response, body)
-
Buffer.new response, body
-
end
-
-
1
def munge_body_object(body)
-
body.respond_to?(:each) ? body : [body]
-
end
-
-
1
def assign_default_content_type_and_charset!
-
return if media_type
-
-
ct = parsed_content_type_header
-
set_content_type(ct.mime_type || Mime[:html].to_s,
-
ct.charset || self.class.default_charset)
-
end
-
-
1
class RackBody
-
1
def initialize(response)
-
@response = response
-
end
-
-
1
def each(*args, &block)
-
@response.each(*args, &block)
-
end
-
-
1
def close
-
# Rack "close" maps to Response#abort, and *not* Response#close
-
# (which is used when the controller's finished writing)
-
@response.abort
-
end
-
-
1
def body
-
@response.body
-
end
-
-
1
def respond_to?(method, include_private = false)
-
if method.to_sym == :to_path
-
@response.stream.respond_to?(method)
-
else
-
super
-
end
-
end
-
-
1
def to_path
-
@response.stream.to_path
-
end
-
-
1
def to_ary
-
nil
-
end
-
end
-
-
1
def handle_no_content!
-
if NO_CONTENT_CODES.include?(@status)
-
@header.delete CONTENT_TYPE
-
@header.delete "Content-Length"
-
end
-
end
-
-
1
def rack_response(status, header)
-
if NO_CONTENT_CODES.include?(status)
-
[status, header, []]
-
else
-
[status, header, RackBody.new(self)]
-
end
-
end
-
end
-
-
1
ActiveSupport.run_load_hooks(:action_dispatch_response, Response)
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/keys"
-
-
1
module ActionDispatch
-
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
-
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
-
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
-
# then expose the flash to its template. Actually, that exposure is automatically done.
-
#
-
# class PostsController < ActionController::Base
-
# def create
-
# # save post
-
# flash[:notice] = "Post successfully created"
-
# redirect_to @post
-
# end
-
#
-
# def show
-
# # doesn't need to assign the flash notice to the template, that's done automatically
-
# end
-
# end
-
#
-
# show.html.erb
-
# <% if flash[:notice] %>
-
# <div class="notice"><%= flash[:notice] %></div>
-
# <% end %>
-
#
-
# Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
-
#
-
# flash.alert = "You must be logged in"
-
# flash.notice = "Post successfully created"
-
#
-
# This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
-
# non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
-
# use sanitize helper.
-
#
-
# Just remember: They'll be gone by the time the next action has been performed.
-
#
-
# See docs on the FlashHash class for more details about the flash.
-
1
class Flash
-
1
KEY = "action_dispatch.request.flash_hash"
-
-
1
module RequestMethods
-
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
-
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
-
# to put a new one.
-
1
def flash
-
flash = flash_hash
-
return flash if flash
-
self.flash = Flash::FlashHash.from_session_value(session["flash"])
-
end
-
-
1
def flash=(flash)
-
set_header Flash::KEY, flash
-
end
-
-
1
def flash_hash # :nodoc:
-
get_header Flash::KEY
-
end
-
-
1
def commit_flash # :nodoc:
-
session = self.session || {}
-
flash_hash = self.flash_hash
-
-
if flash_hash && (flash_hash.present? || session.key?("flash"))
-
session["flash"] = flash_hash.to_session_value
-
self.flash = flash_hash.dup
-
end
-
-
if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded?
-
session.key?("flash") && session["flash"].nil?
-
session.delete("flash")
-
end
-
end
-
-
1
def reset_session # :nodoc:
-
super
-
self.flash = nil
-
end
-
end
-
-
1
class FlashNow #:nodoc:
-
1
attr_accessor :flash
-
-
1
def initialize(flash)
-
@flash = flash
-
end
-
-
1
def []=(k, v)
-
k = k.to_s
-
@flash[k] = v
-
@flash.discard(k)
-
v
-
end
-
-
1
def [](k)
-
@flash[k.to_s]
-
end
-
-
# Convenience accessor for <tt>flash.now[:alert]=</tt>.
-
1
def alert=(message)
-
self[:alert] = message
-
end
-
-
# Convenience accessor for <tt>flash.now[:notice]=</tt>.
-
1
def notice=(message)
-
self[:notice] = message
-
end
-
end
-
-
1
class FlashHash
-
1
include Enumerable
-
-
1
def self.from_session_value(value) #:nodoc:
-
case value
-
when FlashHash # Rails 3.1, 3.2
-
flashes = value.instance_variable_get(:@flashes)
-
if discard = value.instance_variable_get(:@used)
-
flashes.except!(*discard)
-
end
-
new(flashes, flashes.keys)
-
when Hash # Rails 4.0
-
flashes = value["flashes"]
-
if discard = value["discard"]
-
flashes.except!(*discard)
-
end
-
new(flashes, flashes.keys)
-
else
-
new
-
end
-
end
-
-
# Builds a hash containing the flashes to keep for the next request.
-
# If there are none to keep, returns +nil+.
-
1
def to_session_value #:nodoc:
-
flashes_to_keep = @flashes.except(*@discard)
-
return nil if flashes_to_keep.empty?
-
{ "discard" => [], "flashes" => flashes_to_keep }
-
end
-
-
1
def initialize(flashes = {}, discard = []) #:nodoc:
-
@discard = Set.new(stringify_array(discard))
-
@flashes = flashes.stringify_keys
-
@now = nil
-
end
-
-
1
def initialize_copy(other)
-
if other.now_is_loaded?
-
@now = other.now.dup
-
@now.flash = self
-
end
-
super
-
end
-
-
1
def []=(k, v)
-
k = k.to_s
-
@discard.delete k
-
@flashes[k] = v
-
end
-
-
1
def [](k)
-
@flashes[k.to_s]
-
end
-
-
1
def update(h) #:nodoc:
-
@discard.subtract stringify_array(h.keys)
-
@flashes.update h.stringify_keys
-
self
-
end
-
-
1
def keys
-
@flashes.keys
-
end
-
-
1
def key?(name)
-
@flashes.key? name.to_s
-
end
-
-
1
def delete(key)
-
key = key.to_s
-
@discard.delete key
-
@flashes.delete key
-
self
-
end
-
-
1
def to_hash
-
@flashes.dup
-
end
-
-
1
def empty?
-
@flashes.empty?
-
end
-
-
1
def clear
-
@discard.clear
-
@flashes.clear
-
end
-
-
1
def each(&block)
-
@flashes.each(&block)
-
end
-
-
1
alias :merge! :update
-
-
1
def replace(h) #:nodoc:
-
@discard.clear
-
@flashes.replace h.stringify_keys
-
self
-
end
-
-
# Sets a flash that will not be available to the next action, only to the current.
-
#
-
# flash.now[:message] = "Hello current action"
-
#
-
# This method enables you to use the flash as a central messaging system in your app.
-
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
-
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
-
# vanish when the current action is done.
-
#
-
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
-
#
-
# Also, brings two convenience accessors:
-
#
-
# flash.now.alert = "Beware now!"
-
# # Equivalent to flash.now[:alert] = "Beware now!"
-
#
-
# flash.now.notice = "Good luck now!"
-
# # Equivalent to flash.now[:notice] = "Good luck now!"
-
1
def now
-
@now ||= FlashNow.new(self)
-
end
-
-
# Keeps either the entire current flash or a specific flash entry available for the next action:
-
#
-
# flash.keep # keeps the entire flash
-
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
-
1
def keep(k = nil)
-
k = k.to_s if k
-
@discard.subtract Array(k || keys)
-
k ? self[k] : self
-
end
-
-
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
-
#
-
# flash.discard # discard the entire flash at the end of the current action
-
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
-
1
def discard(k = nil)
-
k = k.to_s if k
-
@discard.merge Array(k || keys)
-
k ? self[k] : self
-
end
-
-
# Mark for removal entries that were kept, and delete unkept ones.
-
#
-
# This method is called automatically by filters, so you generally don't need to care about it.
-
1
def sweep #:nodoc:
-
@discard.each { |k| @flashes.delete k }
-
@discard.replace @flashes.keys
-
end
-
-
# Convenience accessor for <tt>flash[:alert]</tt>.
-
1
def alert
-
self[:alert]
-
end
-
-
# Convenience accessor for <tt>flash[:alert]=</tt>.
-
1
def alert=(message)
-
self[:alert] = message
-
end
-
-
# Convenience accessor for <tt>flash[:notice]</tt>.
-
1
def notice
-
self[:notice]
-
end
-
-
# Convenience accessor for <tt>flash[:notice]=</tt>.
-
1
def notice=(message)
-
self[:notice] = message
-
end
-
-
1
protected
-
1
def now_is_loaded?
-
@now
-
end
-
-
1
private
-
1
def stringify_array(array) # :doc:
-
array.map do |item|
-
item.kind_of?(Symbol) ? item.to_s : item
-
end
-
end
-
end
-
-
1
def self.new(app) app; end
-
end
-
-
1
class Request
-
1
prepend Flash::RequestMethods
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rails-dom-testing"
-
-
1
module ActionDispatch
-
1
module Assertions
-
1
autoload :ResponseAssertions, "action_dispatch/testing/assertions/response"
-
1
autoload :RoutingAssertions, "action_dispatch/testing/assertions/routing"
-
-
1
extend ActiveSupport::Concern
-
-
1
include ResponseAssertions
-
1
include RoutingAssertions
-
1
include Rails::Dom::Testing::Assertions
-
-
1
def html_document
-
@html_document ||= if @response.media_type&.end_with?("xml")
-
Nokogiri::XML::Document.parse(@response.body)
-
else
-
Nokogiri::HTML::Document.parse(@response.body)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
1
module Assertions
-
# A small suite of assertions that test responses from \Rails applications.
-
1
module ResponseAssertions
-
RESPONSE_PREDICATES = { # :nodoc:
-
1
success: :successful?,
-
missing: :not_found?,
-
redirect: :redirection?,
-
error: :server_error?,
-
}
-
-
# Asserts that the response is one of the following types:
-
#
-
# * <tt>:success</tt> - Status code was in the 200-299 range
-
# * <tt>:redirect</tt> - Status code was in the 300-399 range
-
# * <tt>:missing</tt> - Status code was 404
-
# * <tt>:error</tt> - Status code was in the 500-599 range
-
#
-
# You can also pass an explicit status number like <tt>assert_response(501)</tt>
-
# or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
-
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
-
#
-
# # Asserts that the response was a redirection
-
# assert_response :redirect
-
#
-
# # Asserts that the response code was status code 401 (unauthorized)
-
# assert_response 401
-
1
def assert_response(type, message = nil)
-
message ||= generate_response_message(type)
-
-
if RESPONSE_PREDICATES.keys.include?(type)
-
assert @response.public_send(RESPONSE_PREDICATES[type]), message
-
else
-
assert_equal AssertionResponse.new(type).code, @response.response_code, message
-
end
-
end
-
-
# Asserts that the response is a redirect to a URL matching the given options.
-
#
-
# # Asserts that the redirection was to the "index" action on the WeblogController
-
# assert_redirected_to controller: "weblog", action: "index"
-
#
-
# # Asserts that the redirection was to the named route login_url
-
# assert_redirected_to login_url
-
#
-
# # Asserts that the redirection was to the URL for @customer
-
# assert_redirected_to @customer
-
#
-
# # Asserts that the redirection matches the regular expression
-
# assert_redirected_to %r(\Ahttp://example.org)
-
1
def assert_redirected_to(options = {}, message = nil)
-
assert_response(:redirect, message)
-
return true if options === @response.location
-
-
redirect_is = normalize_argument_to_redirection(@response.location)
-
redirect_expected = normalize_argument_to_redirection(options)
-
-
message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
-
assert_operator redirect_expected, :===, redirect_is, message
-
end
-
-
1
private
-
# Proxy to to_param if the object will respond to it.
-
1
def parameterize(value)
-
value.respond_to?(:to_param) ? value.to_param : value
-
end
-
-
1
def normalize_argument_to_redirection(fragment)
-
if Regexp === fragment
-
fragment
-
else
-
handle = @controller || ActionController::Redirecting
-
handle._compute_redirect_to_location(@request, fragment)
-
end
-
end
-
-
1
def generate_response_message(expected, actual = @response.response_code)
-
(+"Expected response to be a <#{code_with_name(expected)}>,"\
-
" but was a <#{code_with_name(actual)}>").concat(location_if_redirected).concat(response_body_if_short)
-
end
-
-
1
def response_body_if_short
-
return "" if @response.body.size > 500
-
"\nResponse body: #{@response.body}"
-
end
-
-
1
def location_if_redirected
-
return "" unless @response.redirection? && @response.location.present?
-
location = normalize_argument_to_redirection(@response.location)
-
" redirect to <#{location}>"
-
end
-
-
1
def code_with_name(code_or_name)
-
if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym)
-
code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym]
-
end
-
-
AssertionResponse.new(code_or_name).code_and_name
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "uri"
-
1
require "active_support/core_ext/hash/indifferent_access"
-
1
require "active_support/core_ext/string/access"
-
1
require "action_controller/metal/exceptions"
-
-
1
module ActionDispatch
-
1
module Assertions
-
# Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
-
1
module RoutingAssertions
-
1
def setup # :nodoc:
-
@routes ||= nil
-
super
-
end
-
-
# Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
-
# match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
-
#
-
# Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
-
# requiring a specific HTTP method. The hash should contain a :path with the incoming request path
-
# and a :method containing the required HTTP verb.
-
#
-
# # Asserts that POSTing to /items will call the create action on ItemsController
-
# assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
-
#
-
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
-
# to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
-
# argument because appending the query string on the path directly will not work. For example:
-
#
-
# # Asserts that a path of '/items/list/1?view=print' returns the correct options
-
# assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
-
#
-
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
-
#
-
# # Check the default route (i.e., the index action)
-
# assert_recognizes({controller: 'items', action: 'index'}, 'items')
-
#
-
# # Test a specific action
-
# assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
-
#
-
# # Test an action with a parameter
-
# assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
-
#
-
# # Test a custom route
-
# assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
-
1
def assert_recognizes(expected_options, path, extras = {}, msg = nil)
-
if path.is_a?(Hash) && path[:method].to_s == "all"
-
[:get, :post, :put, :delete].each do |method|
-
assert_recognizes(expected_options, path.merge(method: method), extras, msg)
-
end
-
else
-
request = recognized_request_for(path, extras, msg)
-
-
expected_options = expected_options.clone
-
-
expected_options.stringify_keys!
-
-
msg = message(msg, "") {
-
sprintf("The recognized options <%s> did not match <%s>, difference:",
-
request.path_parameters, expected_options)
-
}
-
-
assert_equal(expected_options, request.path_parameters, msg)
-
end
-
end
-
-
# Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
-
# The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
-
# a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
-
#
-
# The +defaults+ parameter is unused.
-
#
-
# # Asserts that the default action is generated for a route with no action
-
# assert_generates "/items", controller: "items", action: "index"
-
#
-
# # Tests that the list action is properly routed
-
# assert_generates "/items/list", controller: "items", action: "list"
-
#
-
# # Tests the generation of a route with a parameter
-
# assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
-
#
-
# # Asserts that the generated route gives us our custom route
-
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
-
1
def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
-
if %r{://}.match?(expected_path)
-
fail_on(URI::InvalidURIError, message) do
-
uri = URI.parse(expected_path)
-
expected_path = uri.path.to_s.empty? ? "/" : uri.path
-
end
-
else
-
expected_path = "/#{expected_path}" unless expected_path.start_with?("/")
-
end
-
-
options = options.clone
-
generated_path, query_string_keys = @routes.generate_extras(options, defaults)
-
found_extras = options.reject { |k, _| ! query_string_keys.include? k }
-
-
msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
-
assert_equal(extras, found_extras, msg)
-
-
msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path,
-
expected_path)
-
assert_equal(expected_path, generated_path, msg)
-
end
-
-
# Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
-
# <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
-
# and +assert_generates+ into one step.
-
#
-
# The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
-
# +message+ parameter allows you to specify a custom error message to display upon failure.
-
#
-
# # Asserts a basic route: a controller with the default action (index)
-
# assert_routing '/home', controller: 'home', action: 'index'
-
#
-
# # Test a route generated with a specific controller, action, and parameter (id)
-
# assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
-
#
-
# # Asserts a basic route (controller + default action), with an error message if it fails
-
# assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
-
#
-
# # Tests a route, providing a defaults hash
-
# assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
-
#
-
# # Tests a route with an HTTP method
-
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
-
1
def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
-
assert_recognizes(options, path, extras, message)
-
-
controller, default_controller = options[:controller], defaults[:controller]
-
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
-
options[:controller] = "/#{controller}"
-
end
-
-
generate_options = options.dup.delete_if { |k, _| defaults.key?(k) }
-
assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
-
end
-
-
# A helper to make it easier to test different route configurations.
-
# This method temporarily replaces @routes with a new RouteSet instance.
-
#
-
# The new instance is yielded to the passed block. Typically the block
-
# will create some routes using <tt>set.draw { match ... }</tt>:
-
#
-
# with_routing do |set|
-
# set.draw do
-
# resources :users
-
# end
-
# assert_equal "/users", users_path
-
# end
-
#
-
1
def with_routing
-
old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
-
if defined?(@controller) && @controller
-
old_controller, @controller = @controller, @controller.clone
-
_routes = @routes
-
-
@controller.singleton_class.include(_routes.url_helpers)
-
-
if @controller.respond_to? :view_context_class
-
view_context_class = Class.new(@controller.view_context_class) do
-
include _routes.url_helpers
-
end
-
-
custom_view_context = Module.new {
-
define_method(:view_context_class) do
-
view_context_class
-
end
-
}
-
@controller.extend(custom_view_context)
-
end
-
end
-
yield @routes
-
ensure
-
@routes = old_routes
-
if defined?(@controller) && @controller
-
@controller = old_controller
-
end
-
end
-
-
# ROUTES TODO: These assertions should really work in an integration context
-
1
def method_missing(selector, *args, &block)
-
if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
-
@controller.public_send(selector, *args, &block)
-
else
-
super
-
end
-
end
-
-
1
private
-
# Recognizes the route for a given path.
-
1
def recognized_request_for(path, extras = {}, msg)
-
if path.is_a?(Hash)
-
method = path[:method]
-
path = path[:path]
-
else
-
method = :get
-
end
-
-
controller = @controller if defined?(@controller)
-
request = ActionController::TestRequest.create controller&.class
-
-
if %r{://}.match?(path)
-
fail_on(URI::InvalidURIError, msg) do
-
uri = URI.parse(path)
-
request.env["rack.url_scheme"] = uri.scheme || "http"
-
request.host = uri.host if uri.host
-
request.port = uri.port if uri.port
-
request.path = uri.path.to_s.empty? ? "/" : uri.path
-
end
-
else
-
path = "/#{path}" unless path.start_with?("/")
-
request.path = path
-
end
-
-
request.request_method = method if method
-
-
params = fail_on(ActionController::RoutingError, msg) do
-
@routes.recognize_path(path, method: method, extras: extras)
-
end
-
request.path_parameters = params.with_indifferent_access
-
-
request
-
end
-
-
1
def fail_on(exception_class, message)
-
yield
-
rescue exception_class => e
-
raise Minitest::Assertion, message || e.message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "stringio"
-
1
require "uri"
-
1
require "rack/test"
-
1
require "minitest"
-
-
1
require "action_dispatch/testing/request_encoder"
-
-
1
module ActionDispatch
-
1
module Integration #:nodoc:
-
1
module RequestHelpers
-
# Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def get(path, **args)
-
process(:get, path, **args)
-
end
-
-
# Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def post(path, **args)
-
process(:post, path, **args)
-
end
-
-
# Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def patch(path, **args)
-
process(:patch, path, **args)
-
end
-
-
# Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def put(path, **args)
-
process(:put, path, **args)
-
end
-
-
# Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def delete(path, **args)
-
process(:delete, path, **args)
-
end
-
-
# Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def head(path, **args)
-
process(:head, path, **args)
-
end
-
-
# Performs an OPTIONS request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def options(path, **args)
-
process(:options, path, **args)
-
end
-
-
# Follow a single redirect response. If the last response was not a
-
# redirect, an exception will be raised. Otherwise, the redirect is
-
# performed on the location header. If the redirection is a 307 or 308 redirect,
-
# the same HTTP verb will be used when redirecting, otherwise a GET request
-
# will be performed. Any arguments are passed to the
-
# underlying request.
-
1
def follow_redirect!(**args)
-
raise "not a redirect! #{status} #{status_message}" unless redirect?
-
-
method =
-
if [307, 308].include?(response.status)
-
request.method.downcase
-
else
-
:get
-
end
-
-
public_send(method, response.location, **args)
-
status
-
end
-
end
-
-
# An instance of this class represents a set of requests and responses
-
# performed sequentially by a test process. Because you can instantiate
-
# multiple sessions and run them side-by-side, you can also mimic (to some
-
# limited extent) multiple simultaneous users interacting with your system.
-
#
-
# Typically, you will instantiate a new session using
-
# IntegrationTest#open_session, rather than instantiating
-
# Integration::Session directly.
-
1
class Session
-
1
DEFAULT_HOST = "www.example.com"
-
-
1
include Minitest::Assertions
-
1
include TestProcess, RequestHelpers, Assertions
-
-
1
delegate :status, :status_message, :headers, :body, :redirect?, to: :response, allow_nil: true
-
1
delegate :path, to: :request, allow_nil: true
-
-
# The hostname used in the last request.
-
1
def host
-
@host || DEFAULT_HOST
-
end
-
1
attr_writer :host
-
-
# The remote_addr used in the last request.
-
1
attr_accessor :remote_addr
-
-
# The Accept header to send.
-
1
attr_accessor :accept
-
-
# A map of the cookies returned by the last response, and which will be
-
# sent with the next request.
-
1
def cookies
-
_mock_session.cookie_jar
-
end
-
-
# A reference to the controller instance used by the last request.
-
1
attr_reader :controller
-
-
# A reference to the request instance used by the last request.
-
1
attr_reader :request
-
-
# A reference to the response instance used by the last request.
-
1
attr_reader :response
-
-
# A running counter of the number of requests processed.
-
1
attr_accessor :request_count
-
-
1
include ActionDispatch::Routing::UrlFor
-
-
# Create and initialize a new Session instance.
-
1
def initialize(app)
-
super()
-
@app = app
-
-
reset!
-
end
-
-
1
def url_options
-
@url_options ||= default_url_options.dup.tap do |url_options|
-
url_options.reverse_merge!(controller.url_options) if controller.respond_to?(:url_options)
-
-
if @app.respond_to?(:routes)
-
url_options.reverse_merge!(@app.routes.default_url_options)
-
end
-
-
url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
-
end
-
end
-
-
# Resets the instance. This can be used to reset the state information
-
# in an existing session instance, so it can be used from a clean-slate
-
# condition.
-
#
-
# session.reset!
-
1
def reset!
-
@https = false
-
@controller = @request = @response = nil
-
@_mock_session = nil
-
@request_count = 0
-
@url_options = nil
-
-
self.host = DEFAULT_HOST
-
self.remote_addr = "127.0.0.1"
-
self.accept = "text/xml,application/xml,application/xhtml+xml," \
-
"text/html;q=0.9,text/plain;q=0.8,image/png," \
-
"*/*;q=0.5"
-
-
unless defined? @named_routes_configured
-
# the helpers are made protected by default--we make them public for
-
# easier access during testing and troubleshooting.
-
@named_routes_configured = true
-
end
-
end
-
-
# Specify whether or not the session should mimic a secure HTTPS request.
-
#
-
# session.https!
-
# session.https!(false)
-
1
def https!(flag = true)
-
@https = flag
-
end
-
-
# Returns +true+ if the session is mimicking a secure HTTPS request.
-
#
-
# if session.https?
-
# ...
-
# end
-
1
def https?
-
@https
-
end
-
-
# Performs the actual request.
-
#
-
# - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS)
-
# as a symbol.
-
# - +path+: The URI (as a String) on which you want to perform the
-
# request.
-
# - +params+: The HTTP parameters that you want to pass. This may
-
# be +nil+,
-
# a Hash, or a String that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or
-
# <tt>multipart/form-data</tt>).
-
# - +headers+: Additional headers to pass, as a Hash. The headers will be
-
# merged into the Rack env hash.
-
# - +env+: Additional env to pass, as a Hash. The headers will be
-
# merged into the Rack env hash.
-
# - +xhr+: Set to +true+ if you want to make and Ajax request.
-
# Adds request headers characteristic of XMLHttpRequest e.g. HTTP_X_REQUESTED_WITH.
-
# The headers will be merged into the Rack env hash.
-
# - +as+: Used for encoding the request with different content type.
-
# Supports +:json+ by default and will set the appropriate request headers.
-
# The headers will be merged into the Rack env hash.
-
#
-
# This method is rarely used directly. Use +#get+, +#post+, or other standard
-
# HTTP methods in integration tests. +#process+ is only required when using a
-
# request method that doesn't have a method defined in the integration tests.
-
#
-
# This method returns the response status, after performing the request.
-
# Furthermore, if this method was called from an ActionDispatch::IntegrationTest object,
-
# then that object's <tt>@response</tt> instance variable will point to a Response object
-
# which one can use to inspect the details of the response.
-
#
-
# Example:
-
# process :get, '/author', params: { since: 201501011400 }
-
1
def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
-
request_encoder = RequestEncoder.encoder(as)
-
headers ||= {}
-
-
if method == :get && as == :json && params
-
headers["X-Http-Method-Override"] = "GET"
-
method = :post
-
end
-
-
if %r{://}.match?(path)
-
path = build_expanded_path(path) do |location|
-
https! URI::HTTPS === location if location.scheme
-
-
if url_host = location.host
-
default = Rack::Request::DEFAULT_PORTS[location.scheme]
-
url_host += ":#{location.port}" if default != location.port
-
host! url_host
-
end
-
end
-
end
-
-
hostname, port = host.split(":")
-
-
request_env = {
-
:method => method,
-
:params => request_encoder.encode_params(params),
-
-
"SERVER_NAME" => hostname,
-
"SERVER_PORT" => port || (https? ? "443" : "80"),
-
"HTTPS" => https? ? "on" : "off",
-
"rack.url_scheme" => https? ? "https" : "http",
-
-
"REQUEST_URI" => path,
-
"HTTP_HOST" => host,
-
"REMOTE_ADDR" => remote_addr,
-
"CONTENT_TYPE" => request_encoder.content_type,
-
"HTTP_ACCEPT" => request_encoder.accept_header || accept
-
}
-
-
wrapped_headers = Http::Headers.from_hash({})
-
wrapped_headers.merge!(headers) if headers
-
-
if xhr
-
wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
-
wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
-
end
-
-
# This modifies the passed request_env directly.
-
if wrapped_headers.present?
-
Http::Headers.from_hash(request_env).merge!(wrapped_headers)
-
end
-
if env.present?
-
Http::Headers.from_hash(request_env).merge!(env)
-
end
-
-
session = Rack::Test::Session.new(_mock_session)
-
-
# NOTE: rack-test v0.5 doesn't build a default uri correctly
-
# Make sure requested path is always a full URI.
-
session.request(build_full_uri(path, request_env), request_env)
-
-
@request_count += 1
-
@request = ActionDispatch::Request.new(session.last_request.env)
-
response = _mock_session.last_response
-
@response = ActionDispatch::TestResponse.from_response(response)
-
@response.request = @request
-
@html_document = nil
-
@url_options = nil
-
-
@controller = @request.controller_instance
-
-
response.status
-
end
-
-
# Set the host name to use in the next request.
-
#
-
# session.host! "www.example.com"
-
1
alias :host! :host=
-
-
1
private
-
1
def _mock_session
-
@_mock_session ||= Rack::MockSession.new(@app, host)
-
end
-
-
1
def build_full_uri(path, env)
-
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
-
end
-
-
1
def build_expanded_path(path)
-
location = URI.parse(path)
-
yield location if block_given?
-
path = location.path
-
location.query ? "#{path}?#{location.query}" : path
-
end
-
end
-
-
1
module Runner
-
1
include ActionDispatch::Assertions
-
-
1
APP_SESSIONS = {}
-
-
1
attr_reader :app
-
1
attr_accessor :root_session # :nodoc:
-
-
1
def initialize(*args, &blk)
-
super(*args, &blk)
-
@integration_session = nil
-
end
-
-
1
def before_setup # :nodoc:
-
@app = nil
-
super
-
end
-
-
1
def integration_session
-
@integration_session ||= create_session(app)
-
end
-
-
# Reset the current session. This is useful for testing multiple sessions
-
# in a single test case.
-
1
def reset!
-
@integration_session = create_session(app)
-
end
-
-
1
def create_session(app)
-
klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
-
# If the app is a Rails app, make url_helpers available on the session.
-
# This makes app.url_for and app.foo_path available in the console.
-
if app.respond_to?(:routes) && app.routes.is_a?(ActionDispatch::Routing::RouteSet)
-
include app.routes.url_helpers
-
include app.routes.mounted_helpers
-
end
-
}
-
klass.new(app)
-
end
-
-
1
def remove! # :nodoc:
-
@integration_session = nil
-
end
-
-
1
%w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
-
# reset the html_document variable, except for cookies/assigns calls
-
9
unless method == "cookies" || method == "assigns"
-
7
reset_html_document = "@html_document = nil"
-
end
-
-
9
definition = RUBY_VERSION >= "2.7" ? "..." : "*args"
-
-
9
module_eval <<~RUBY, __FILE__, __LINE__ + 1
-
def #{method}(#{definition})
-
#{reset_html_document}
-
-
result = integration_session.#{method}(#{definition})
-
copy_session_variables!
-
result
-
end
-
RUBY
-
end
-
-
# Open a new session instance. If a block is given, the new session is
-
# yielded to the block before being returned.
-
#
-
# session = open_session do |sess|
-
# sess.extend(CustomAssertions)
-
# end
-
#
-
# By default, a single session is automatically created for you, but you
-
# can use this method to open multiple sessions that ought to be tested
-
# simultaneously.
-
1
def open_session
-
dup.tap do |session|
-
session.reset!
-
session.root_session = self.root_session || self
-
yield session if block_given?
-
end
-
end
-
-
1
def assertions # :nodoc:
-
root_session ? root_session.assertions : super
-
end
-
-
1
def assertions=(assertions) # :nodoc:
-
root_session ? root_session.assertions = assertions : super
-
end
-
-
# Copy the instance variables from the current session instance into the
-
# test instance.
-
1
def copy_session_variables! #:nodoc:
-
@controller = @integration_session.controller
-
@response = @integration_session.response
-
@request = @integration_session.request
-
end
-
-
1
def default_url_options
-
integration_session.default_url_options
-
end
-
-
1
def default_url_options=(options)
-
integration_session.default_url_options = options
-
end
-
-
1
private
-
1
def respond_to_missing?(method, _)
-
integration_session.respond_to?(method) || super
-
end
-
-
# Delegate unhandled messages to the current session instance.
-
1
def method_missing(method, *args, &block)
-
if integration_session.respond_to?(method)
-
integration_session.public_send(method, *args, &block).tap do
-
copy_session_variables!
-
end
-
else
-
super
-
end
-
end
-
1
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
-
end
-
end
-
-
# An integration test spans multiple controllers and actions,
-
# tying them all together to ensure they work together as expected. It tests
-
# more completely than either unit or functional tests do, exercising the
-
# entire stack, from the dispatcher to the database.
-
#
-
# At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
-
# using the get/post methods:
-
#
-
# require "test_helper"
-
#
-
# class ExampleTest < ActionDispatch::IntegrationTest
-
# fixtures :people
-
#
-
# def test_login
-
# # get the login page
-
# get "/login"
-
# assert_equal 200, status
-
#
-
# # post the login and follow through to the home page
-
# post "/login", params: { username: people(:jamis).username,
-
# password: people(:jamis).password }
-
# follow_redirect!
-
# assert_equal 200, status
-
# assert_equal "/home", path
-
# end
-
# end
-
#
-
# However, you can also have multiple session instances open per test, and
-
# even extend those instances with assertions and methods to create a very
-
# powerful testing DSL that is specific for your application. You can even
-
# reference any named routes you happen to have defined.
-
#
-
# require "test_helper"
-
#
-
# class AdvancedTest < ActionDispatch::IntegrationTest
-
# fixtures :people, :rooms
-
#
-
# def test_login_and_speak
-
# jamis, david = login(:jamis), login(:david)
-
# room = rooms(:office)
-
#
-
# jamis.enter(room)
-
# jamis.speak(room, "anybody home?")
-
#
-
# david.enter(room)
-
# david.speak(room, "hello!")
-
# end
-
#
-
# private
-
#
-
# module CustomAssertions
-
# def enter(room)
-
# # reference a named route, for maximum internal consistency!
-
# get(room_url(id: room.id))
-
# assert(...)
-
# ...
-
# end
-
#
-
# def speak(room, message)
-
# post "/say/#{room.id}", xhr: true, params: { message: message }
-
# assert(...)
-
# ...
-
# end
-
# end
-
#
-
# def login(who)
-
# open_session do |sess|
-
# sess.extend(CustomAssertions)
-
# who = people(who)
-
# sess.post "/login", params: { username: who.username,
-
# password: who.password }
-
# assert(...)
-
# end
-
# end
-
# end
-
#
-
# Another longer example would be:
-
#
-
# A simple integration test that exercises multiple controllers:
-
#
-
# require "test_helper"
-
#
-
# class UserFlowsTest < ActionDispatch::IntegrationTest
-
# test "login and browse site" do
-
# # login via https
-
# https!
-
# get "/login"
-
# assert_response :success
-
#
-
# post "/login", params: { username: users(:david).username, password: users(:david).password }
-
# follow_redirect!
-
# assert_equal '/welcome', path
-
# assert_equal 'Welcome david!', flash[:notice]
-
#
-
# https!(false)
-
# get "/articles/all"
-
# assert_response :success
-
# assert_select 'h1', 'Articles'
-
# end
-
# end
-
#
-
# As you can see the integration test involves multiple controllers and
-
# exercises the entire stack from database to dispatcher. In addition you can
-
# have multiple session instances open simultaneously in a test and extend
-
# those instances with assertion methods to create a very powerful testing
-
# DSL (domain-specific language) just for your application.
-
#
-
# Here's an example of multiple sessions and custom DSL in an integration test
-
#
-
# require "test_helper"
-
#
-
# class UserFlowsTest < ActionDispatch::IntegrationTest
-
# test "login and browse site" do
-
# # User david logs in
-
# david = login(:david)
-
# # User guest logs in
-
# guest = login(:guest)
-
#
-
# # Both are now available in different sessions
-
# assert_equal 'Welcome david!', david.flash[:notice]
-
# assert_equal 'Welcome guest!', guest.flash[:notice]
-
#
-
# # User david can browse site
-
# david.browses_site
-
# # User guest can browse site as well
-
# guest.browses_site
-
#
-
# # Continue with other assertions
-
# end
-
#
-
# private
-
#
-
# module CustomDsl
-
# def browses_site
-
# get "/products/all"
-
# assert_response :success
-
# assert_select 'h1', 'Products'
-
# end
-
# end
-
#
-
# def login(user)
-
# open_session do |sess|
-
# sess.extend(CustomDsl)
-
# u = users(user)
-
# sess.https!
-
# sess.post "/login", params: { username: u.username, password: u.password }
-
# assert_equal '/welcome', sess.path
-
# sess.https!(false)
-
# end
-
# end
-
# end
-
#
-
# See the {request helpers documentation}[rdoc-ref:ActionDispatch::Integration::RequestHelpers] for help on how to
-
# use +get+, etc.
-
#
-
# === Changing the request encoding
-
#
-
# You can also test your JSON API easily by setting what the request should
-
# be encoded as:
-
#
-
# require "test_helper"
-
#
-
# class ApiTest < ActionDispatch::IntegrationTest
-
# test "creates articles" do
-
# assert_difference -> { Article.count } do
-
# post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
-
# end
-
#
-
# assert_response :success
-
# assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
-
# end
-
# end
-
#
-
# The +as+ option passes an "application/json" Accept header (thereby setting
-
# the request format to JSON unless overridden), sets the content type to
-
# "application/json" and encodes the parameters as JSON.
-
#
-
# Calling +parsed_body+ on the response parses the response body based on the
-
# last response MIME type.
-
#
-
# Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
-
# types you've registered, you can add your own encoders with:
-
#
-
# ActionDispatch::IntegrationTest.register_encoder :wibble,
-
# param_encoder: -> params { params.to_wibble },
-
# response_parser: -> body { body }
-
#
-
# Where +param_encoder+ defines how the params should be encoded and
-
# +response_parser+ defines how the response body should be parsed through
-
# +parsed_body+.
-
#
-
# Consult the Rails Testing Guide for more.
-
-
1
class IntegrationTest < ActiveSupport::TestCase
-
1
include TestProcess::FixtureFile
-
-
1
module UrlOptions
-
1
extend ActiveSupport::Concern
-
1
def url_options
-
integration_session.url_options
-
end
-
end
-
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
-
1
include Integration::Runner
-
1
include ActionController::TemplateAssertions
-
-
1
included do
-
1
include ActionDispatch::Routing::UrlFor
-
1
include UrlOptions # don't let UrlFor override the url_options method
-
1
ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
-
1
@@app = nil
-
end
-
-
1
module ClassMethods
-
1
def app
-
if defined?(@@app) && @@app
-
@@app
-
else
-
ActionDispatch.test_app
-
end
-
end
-
-
1
def app=(app)
-
@@app = app
-
end
-
-
1
def register_encoder(*args, **options)
-
RequestEncoder.register_encoder(*args, **options)
-
end
-
end
-
-
1
def app
-
super || self.class.app
-
end
-
-
1
def document_root_element
-
html_document.root
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
1
class RequestEncoder # :nodoc:
-
1
class IdentityEncoder
-
1
def content_type; end
-
1
def accept_header; end
-
1
def encode_params(params); params; end
-
1
def response_parser; -> body { body }; end
-
end
-
-
1
@encoders = { identity: IdentityEncoder.new }
-
-
1
attr_reader :response_parser
-
-
1
def initialize(mime_name, param_encoder, response_parser)
-
1
@mime = Mime[mime_name]
-
-
1
unless @mime
-
raise ArgumentError, "Can't register a request encoder for " \
-
"unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
-
end
-
-
1
@response_parser = response_parser || -> body { body }
-
1
@param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
-
end
-
-
1
def content_type
-
@mime.to_s
-
end
-
-
1
def accept_header
-
@mime.to_s
-
end
-
-
1
def encode_params(params)
-
@param_encoder.call(params) if params
-
end
-
-
1
def self.parser(content_type)
-
type = Mime::Type.lookup(content_type).ref if content_type
-
encoder(type).response_parser
-
end
-
-
1
def self.encoder(name)
-
@encoders[name] || @encoders[:identity]
-
end
-
-
1
def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
-
1
@encoders[mime_name] = new(mime_name, param_encoder, response_parser)
-
end
-
-
1
register_encoder :json, response_parser: -> body { JSON.parse(body) }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/middleware/cookies"
-
1
require "action_dispatch/middleware/flash"
-
-
1
module ActionDispatch
-
1
module TestProcess
-
1
module FixtureFile
-
# Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.file_fixture_path, path), type)</tt>:
-
#
-
# post :change_avatar, params: { avatar: fixture_file_upload('spongebob.png', 'image/png') }
-
#
-
# Default fixture files location is <tt>test/fixtures/files</tt>.
-
#
-
# To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
-
# This will not affect other platforms:
-
#
-
# post :change_avatar, params: { avatar: fixture_file_upload('spongebob.png', 'image/png', :binary) }
-
1
def fixture_file_upload(path, mime_type = nil, binary = false)
-
if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
-
!File.exist?(path)
-
original_path = path
-
path = Pathname.new(self.class.fixture_path).join(path)
-
-
if !self.class.file_fixture_path
-
ActiveSupport::Deprecation.warn(<<~EOM)
-
Passing a path to `fixture_file_upload` relative to `fixture_path` is deprecated.
-
In Rails 6.2, the path needs to be relative to `file_fixture_path` which you
-
haven't set yet. Set `file_fixture_path` to discard this warning.
-
EOM
-
elsif path.exist?
-
non_deprecated_path = Pathname(File.absolute_path(path)).relative_path_from(Pathname(File.absolute_path(self.class.file_fixture_path)))
-
ActiveSupport::Deprecation.warn(<<~EOM)
-
Passing a path to `fixture_file_upload` relative to `fixture_path` is deprecated.
-
In Rails 6.2, the path needs to be relative to `file_fixture_path`.
-
-
Please modify the call from
-
`fixture_file_upload("#{original_path}")` to `fixture_file_upload("#{non_deprecated_path}")`.
-
EOM
-
else
-
path = file_fixture(original_path)
-
end
-
elsif self.class.file_fixture_path && !File.exist?(path)
-
path = file_fixture(path)
-
end
-
-
Rack::Test::UploadedFile.new(path, mime_type, binary)
-
end
-
end
-
-
1
include FixtureFile
-
-
1
def assigns(key = nil)
-
raise NoMethodError,
-
"assigns has been extracted to a gem. To continue using it,
-
add `gem 'rails-controller-testing'` to your Gemfile."
-
end
-
-
1
def session
-
@request.session
-
end
-
-
1
def flash
-
@request.flash
-
end
-
-
1
def cookies
-
@cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
-
end
-
-
1
def redirect_to_url
-
@response.redirect_url
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/indifferent_access"
-
1
require "rack/utils"
-
-
1
module ActionDispatch
-
1
class TestRequest < Request
-
1
DEFAULT_ENV = Rack::MockRequest.env_for("/",
-
"HTTP_HOST" => "test.host".b,
-
"REMOTE_ADDR" => "0.0.0.0".b,
-
"HTTP_USER_AGENT" => "Rails Testing".b,
-
)
-
-
# Create a new test request with default +env+ values.
-
1
def self.create(env = {})
-
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
-
env["rack.request.cookie_hash"] ||= {}.with_indifferent_access
-
new(default_env.merge(env))
-
end
-
-
1
def self.default_env
-
DEFAULT_ENV
-
end
-
1
private_class_method :default_env
-
-
1
def request_method=(method)
-
super(method.to_s.upcase)
-
end
-
-
1
def host=(host)
-
set_header("HTTP_HOST", host)
-
end
-
-
1
def port=(number)
-
set_header("SERVER_PORT", number.to_i)
-
end
-
-
1
def request_uri=(uri)
-
set_header("REQUEST_URI", uri)
-
end
-
-
1
def path=(path)
-
set_header("PATH_INFO", path)
-
end
-
-
1
def action=(action_name)
-
path_parameters[:action] = action_name.to_s
-
end
-
-
1
def if_modified_since=(last_modified)
-
set_header("HTTP_IF_MODIFIED_SINCE", last_modified)
-
end
-
-
1
def if_none_match=(etag)
-
set_header("HTTP_IF_NONE_MATCH", etag)
-
end
-
-
1
def remote_addr=(addr)
-
set_header("REMOTE_ADDR", addr)
-
end
-
-
1
def user_agent=(user_agent)
-
set_header("HTTP_USER_AGENT", user_agent)
-
end
-
-
1
def accept=(mime_types)
-
delete_header("action_dispatch.request.accepts")
-
set_header("HTTP_ACCEPT", Array(mime_types).collect(&:to_s).join(","))
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rails-html-sanitizer"
-
-
1
module ActionText
-
1
module ContentHelper
-
2
mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.safe_list_sanitizer.new }
-
2
mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] }
-
2
mattr_accessor(:allowed_attributes) { sanitizer.class.allowed_attributes + ActionText::Attachment::ATTRIBUTES }
-
1
mattr_accessor(:scrubber)
-
-
1
def render_action_text_content(content)
-
self.prefix_partial_path_with_controller_namespace = false
-
sanitize_action_text_content(render_action_text_attachments(content))
-
end
-
-
1
def sanitize_action_text_content(content)
-
sanitizer.sanitize(content.to_html, tags: allowed_tags, attributes: allowed_attributes, scrubber: scrubber).html_safe
-
end
-
-
1
def render_action_text_attachments(content)
-
content.render_attachments do |attachment|
-
unless attachment.in?(content.gallery_attachments)
-
attachment.node.tap do |node|
-
node.inner_html = render(attachment, in_gallery: false).chomp
-
end
-
end
-
end.render_attachment_galleries do |attachment_gallery|
-
render(layout: attachment_gallery, object: attachment_gallery) do
-
attachment_gallery.attachments.map do |attachment|
-
attachment.node.inner_html = render(attachment, in_gallery: true).chomp
-
attachment.to_html
-
end.join.html_safe
-
end.chomp
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/object/try"
-
1
require "action_view/helpers/tags/placeholderable"
-
-
1
module ActionText
-
1
module TagHelper
-
2
cattr_accessor(:id, instance_accessor: false) { 0 }
-
-
# Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
-
# that Trix will write to on changes, so the content will be sent on form submissions.
-
#
-
# ==== Options
-
# * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
-
#
-
# ==== Example
-
#
-
# rich_text_area_tag "content", message.content
-
# # <input type="hidden" name="content" id="trix_input_post_1">
-
# # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
-
1
def rich_text_area_tag(name, value = nil, options = {})
-
options = options.symbolize_keys
-
-
options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
-
options[:class] ||= "trix-content"
-
-
options[:data] ||= {}
-
options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url
-
options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename")
-
-
editor_tag = content_tag("trix-editor", "", options)
-
input_tag = hidden_field_tag(name, value, id: options[:input])
-
-
input_tag + editor_tag
-
end
-
end
-
end
-
-
1
module ActionView::Helpers
-
1
class Tags::ActionText < Tags::Base
-
1
include Tags::Placeholderable
-
-
1
delegate :dom_id, to: ActionView::RecordIdentifier
-
-
1
def render
-
options = @options.stringify_keys
-
add_default_name_and_id(options)
-
options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
-
@template_object.rich_text_area_tag(options.delete("name"), options.fetch("value") { editable_value }, options.except("value"))
-
end
-
-
1
def editable_value
-
value&.body.try(:to_trix_html)
-
end
-
end
-
-
1
module FormHelper
-
# Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
-
# that Trix will write to on changes, so the content will be sent on form submissions.
-
#
-
# ==== Options
-
# * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
-
# * <tt>:value</tt> - Adds a default value to the HTML input tag.
-
#
-
# ==== Example
-
# form_with(model: @message) do |form|
-
# form.rich_text_area :content
-
# end
-
# # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1">
-
# # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
-
#
-
# form_with(model: @message) do |form|
-
# form.rich_text_area :content, value: "<h1>Default message</h1>"
-
# end
-
# # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1" value="<h1>Default message</h1>">
-
# # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
-
1
def rich_text_area(object_name, method, options = {})
-
Tags::ActionText.new(object_name, method, self, options).render
-
end
-
end
-
-
1
class FormBuilder
-
1
def rich_text_area(method, options = {})
-
@template.rich_text_area(@object_name, method, objectify_options(options))
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/object/try"
-
-
1
module ActionText
-
1
class Attachment
-
1
include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
-
-
1
TAG_NAME = "action-text-attachment"
-
1
SELECTOR = TAG_NAME
-
1
ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption )
-
-
1
class << self
-
1
def fragment_by_canonicalizing_attachments(content)
-
fragment_by_minifying_attachments(fragment_by_converting_trix_attachments(content))
-
end
-
-
1
def from_node(node, attachable = nil)
-
new(node, attachable || ActionText::Attachable.from_node(node))
-
end
-
-
1
def from_attachables(attachables)
-
Array(attachables).map { |attachable| from_attachable(attachable) }.compact
-
end
-
-
1
def from_attachable(attachable, attributes = {})
-
if node = node_from_attributes(attachable.to_rich_text_attributes(attributes))
-
new(node, attachable)
-
end
-
end
-
-
1
def from_attributes(attributes, attachable = nil)
-
if node = node_from_attributes(attributes)
-
from_node(node, attachable)
-
end
-
end
-
-
1
private
-
1
def node_from_attributes(attributes)
-
if attributes = process_attributes(attributes).presence
-
ActionText::HtmlConversion.create_element(TAG_NAME, attributes)
-
end
-
end
-
-
1
def process_attributes(attributes)
-
attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES)
-
end
-
end
-
-
1
attr_reader :node, :attachable
-
-
1
delegate :to_param, to: :attachable
-
1
delegate_missing_to :attachable
-
-
1
def initialize(node, attachable)
-
@node = node
-
@attachable = attachable
-
end
-
-
1
def caption
-
node_attributes["caption"].presence
-
end
-
-
1
def full_attributes
-
node_attributes.merge(attachable_attributes).merge(sgid_attributes)
-
end
-
-
1
def with_full_attributes
-
self.class.from_attributes(full_attributes, attachable)
-
end
-
-
1
def to_plain_text
-
if respond_to?(:attachable_plain_text_representation)
-
attachable_plain_text_representation(caption)
-
else
-
caption.to_s
-
end
-
end
-
-
1
def to_html
-
HtmlConversion.node_to_html(node)
-
end
-
-
1
def to_s
-
to_html
-
end
-
-
1
def inspect
-
"#<#{self.class.name} attachable=#{attachable.inspect}>"
-
end
-
-
1
private
-
1
def node_attributes
-
@node_attributes ||= ATTRIBUTES.map { |name| [ name.underscore, node[name] ] }.to_h.compact
-
end
-
-
1
def attachable_attributes
-
@attachable_attributes ||= (attachable.try(:to_rich_text_attributes) || {}).stringify_keys
-
end
-
-
1
def sgid_attributes
-
@sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionText
-
1
module Attachments
-
1
module Caching
-
1
def cache_key(*args)
-
[self.class.name, cache_digest, *attachable.cache_key(*args)].join("/")
-
end
-
-
1
private
-
1
def cache_digest
-
Digest::SHA256.hexdigest(node.to_s)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionText
-
1
module Attachments
-
1
module Minification
-
1
extend ActiveSupport::Concern
-
-
1
class_methods do
-
1
def fragment_by_minifying_attachments(content)
-
Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node|
-
node.tap { |n| n.inner_html = "" }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/object/try"
-
-
1
module ActionText
-
1
module Attachments
-
1
module TrixConversion
-
1
extend ActiveSupport::Concern
-
-
1
class_methods do
-
1
def fragment_by_converting_trix_attachments(content)
-
Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node|
-
from_trix_attachment(TrixAttachment.new(node))
-
end
-
end
-
-
1
def from_trix_attachment(trix_attachment)
-
from_attributes(trix_attachment.attributes)
-
end
-
end
-
-
1
def to_trix_attachment(content = trix_attachment_content)
-
attributes = full_attributes.dup
-
attributes["content"] = content if content
-
TrixAttachment.from_attributes(attributes)
-
end
-
-
1
private
-
1
def trix_attachment_content
-
if partial_path = attachable.try(:to_trix_content_attachment_partial_path)
-
ActionText::Content.render(partial: partial_path, object: self, as: model_name.element)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
# = Action View Context
-
#
-
# Action View contexts are supplied to Action Controller to render a template.
-
# The default Action View context is ActionView::Base.
-
#
-
# In order to work with Action Controller, a Context must just include this
-
# module. The initialization of the variables used by the context
-
# (@output_buffer, @view_flow, and @virtual_path) is responsibility of the
-
# object that includes this module (although you can call _prepare_context
-
# defined below).
-
1
module Context
-
1
attr_accessor :output_buffer, :view_flow
-
-
# Prepares the context by setting the appropriate instance variables.
-
1
def _prepare_context
-
@view_flow = OutputFlow.new
-
@output_buffer = nil
-
@virtual_path = nil
-
end
-
-
# Encapsulates the interaction with the view flow so it
-
# returns the correct buffer on +yield+. This is usually
-
# overwritten by helpers to add more behavior.
-
1
def _layout_for(name = nil)
-
name ||= :layout
-
view_flow.get(name).html_safe
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module Helpers #:nodoc:
-
1
module Tags #:nodoc:
-
1
extend ActiveSupport::Autoload
-
-
1
eager_autoload do
-
1
autoload :Base
-
1
autoload :Translator
-
1
autoload :CheckBox
-
1
autoload :CollectionCheckBoxes
-
1
autoload :CollectionRadioButtons
-
1
autoload :CollectionSelect
-
1
autoload :ColorField
-
1
autoload :DateField
-
1
autoload :DateSelect
-
1
autoload :DatetimeField
-
1
autoload :DatetimeLocalField
-
1
autoload :DatetimeSelect
-
1
autoload :EmailField
-
1
autoload :FileField
-
1
autoload :GroupedCollectionSelect
-
1
autoload :HiddenField
-
1
autoload :Label
-
1
autoload :MonthField
-
1
autoload :NumberField
-
1
autoload :PasswordField
-
1
autoload :RadioButton
-
1
autoload :RangeField
-
1
autoload :SearchField
-
1
autoload :Select
-
1
autoload :TelField
-
1
autoload :TextArea
-
1
autoload :TextField
-
1
autoload :TimeField
-
1
autoload :TimeSelect
-
1
autoload :TimeZoneSelect
-
1
autoload :UrlField
-
1
autoload :WeekField
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
class Base # :nodoc:
-
1
include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
-
1
include FormOptionsHelper
-
-
1
attr_reader :object
-
-
1
def initialize(object_name, method_name, template_object, options = {})
-
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
-
@template_object = template_object
-
-
@object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
-
@object = retrieve_object(options.delete(:object))
-
@skip_default_ids = options.delete(:skip_default_ids)
-
@allow_method_names_outside_object = options.delete(:allow_method_names_outside_object)
-
@options = options
-
-
if Regexp.last_match
-
@generate_indexed_names = true
-
@auto_index = retrieve_autoindex(Regexp.last_match.pre_match)
-
else
-
@generate_indexed_names = false
-
@auto_index = nil
-
end
-
end
-
-
# This is what child classes implement.
-
1
def render
-
raise NotImplementedError, "Subclasses must implement a render method"
-
end
-
-
1
private
-
1
def value
-
if @allow_method_names_outside_object
-
object.public_send @method_name if object && object.respond_to?(@method_name)
-
else
-
object.public_send @method_name if object
-
end
-
end
-
-
1
def value_before_type_cast
-
unless object.nil?
-
method_before_type_cast = @method_name + "_before_type_cast"
-
-
if value_came_from_user? && object.respond_to?(method_before_type_cast)
-
object.public_send(method_before_type_cast)
-
else
-
value
-
end
-
end
-
end
-
-
1
def value_came_from_user?
-
method_name = "#{@method_name}_came_from_user?"
-
!object.respond_to?(method_name) || object.public_send(method_name)
-
end
-
-
1
def retrieve_object(object)
-
if object
-
object
-
elsif @template_object.instance_variable_defined?("@#{@object_name}")
-
@template_object.instance_variable_get("@#{@object_name}")
-
end
-
rescue NameError
-
# As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
-
nil
-
end
-
-
1
def retrieve_autoindex(pre_match)
-
object = self.object || @template_object.instance_variable_get("@#{pre_match}")
-
if object && object.respond_to?(:to_param)
-
object.to_param
-
else
-
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
-
end
-
end
-
-
1
def add_default_name_and_id_for_value(tag_value, options)
-
if tag_value.nil?
-
add_default_name_and_id(options)
-
else
-
specified_id = options["id"]
-
add_default_name_and_id(options)
-
-
if specified_id.blank? && options["id"].present?
-
options["id"] += "_#{sanitized_value(tag_value)}"
-
end
-
end
-
end
-
-
1
def add_default_name_and_id(options)
-
index = name_and_id_index(options)
-
options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
-
-
if generate_ids?
-
options["id"] = options.fetch("id") { tag_id(index) }
-
if namespace = options.delete("namespace")
-
options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
-
end
-
end
-
end
-
-
1
def tag_name(multiple = false, index = nil)
-
# a little duplication to construct fewer strings
-
case
-
when @object_name.empty?
-
"#{sanitized_method_name}#{multiple ? "[]" : ""}"
-
when index
-
"#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
-
else
-
"#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
-
end
-
end
-
-
1
def tag_id(index = nil)
-
# a little duplication to construct fewer strings
-
case
-
when @object_name.empty?
-
sanitized_method_name.dup
-
when index
-
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
-
else
-
"#{sanitized_object_name}_#{sanitized_method_name}"
-
end
-
end
-
-
1
def sanitized_object_name
-
@sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
-
end
-
-
1
def sanitized_method_name
-
@sanitized_method_name ||= @method_name.delete_suffix("?")
-
end
-
-
1
def sanitized_value(value)
-
value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
-
end
-
-
1
def select_content_tag(option_tags, options, html_options)
-
html_options = html_options.stringify_keys
-
add_default_name_and_id(html_options)
-
-
if placeholder_required?(html_options)
-
raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
-
options[:include_blank] ||= true unless options[:prompt]
-
end
-
-
value = options.fetch(:selected) { value() }
-
select = content_tag("select", add_options(option_tags, options, value), html_options)
-
-
if html_options["multiple"] && options.fetch(:include_hidden, true)
-
tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select
-
else
-
select
-
end
-
end
-
-
1
def placeholder_required?(html_options)
-
# See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
-
html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
-
end
-
-
1
def add_options(option_tags, options, value = nil)
-
if options[:include_blank]
-
content = (options[:include_blank] if options[:include_blank].is_a?(String))
-
label = (" " unless content)
-
option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
-
end
-
-
if value.blank? && options[:prompt]
-
tag_options = { value: "" }.tap do |prompt_opts|
-
prompt_opts[:disabled] = true if options[:disabled] == ""
-
prompt_opts[:selected] = true if options[:selected] == ""
-
end
-
option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
-
end
-
-
option_tags
-
end
-
-
1
def name_and_id_index(options)
-
if options.key?("index")
-
options.delete("index") || ""
-
elsif @generate_indexed_names
-
@auto_index || ""
-
end
-
end
-
-
1
def generate_ids?
-
!@skip_default_ids
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
module Placeholderable # :nodoc:
-
1
def initialize(*)
-
super
-
-
if tag_value = @options[:placeholder]
-
placeholder = tag_value if tag_value.is_a?(String)
-
method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
-
-
placeholder ||= Tags::Translator
-
.new(object, @object_name, method_and_value, scope: "helpers.placeholder")
-
.translate
-
placeholder ||= @method_name.humanize
-
@options[:placeholder] = placeholder
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_view/rendering"
-
1
require "active_support/core_ext/module/redefine_method"
-
-
1
module ActionView
-
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
-
# repeated setups. The inclusion pattern has pages that look like this:
-
#
-
# <%= render "shared/header" %>
-
# Hello World
-
# <%= render "shared/footer" %>
-
#
-
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
-
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
-
#
-
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
-
# that the header and footer are only mentioned in one place, like this:
-
#
-
# // The header part of this layout
-
# <%= yield %>
-
# // The footer part of this layout
-
#
-
# And then you have content pages that look like this:
-
#
-
# hello world
-
#
-
# At rendering time, the content page is computed and then inserted in the layout, like this:
-
#
-
# // The header part of this layout
-
# hello world
-
# // The footer part of this layout
-
#
-
# == Accessing shared variables
-
#
-
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
-
# references that won't materialize before rendering time:
-
#
-
# <h1><%= @page_title %></h1>
-
# <%= yield %>
-
#
-
# ...and content pages that fulfill these references _at_ rendering time:
-
#
-
# <% @page_title = "Welcome" %>
-
# Off-world colonies offers you a chance to start a new life
-
#
-
# The result after rendering is:
-
#
-
# <h1>Welcome</h1>
-
# Off-world colonies offers you a chance to start a new life
-
#
-
# == Layout assignment
-
#
-
# You can either specify a layout declaratively (using the #layout class method) or give
-
# it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
-
# If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
-
#
-
# For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
-
# that template will be used for all actions in PostsController and controllers inheriting
-
# from PostsController.
-
#
-
# If you use a module, for instance Weblog::PostsController, you will need a template named
-
# <tt>app/views/layouts/weblog/posts.html.erb</tt>.
-
#
-
# Since all your controllers inherit from ApplicationController, they will use
-
# <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
-
# or provided.
-
#
-
# == Inheritance Examples
-
#
-
# class BankController < ActionController::Base
-
# # bank.html.erb exists
-
#
-
# class ExchangeController < BankController
-
# # exchange.html.erb exists
-
#
-
# class CurrencyController < BankController
-
#
-
# class InformationController < BankController
-
# layout "information"
-
#
-
# class TellerController < InformationController
-
# # teller.html.erb exists
-
#
-
# class EmployeeController < InformationController
-
# # employee.html.erb exists
-
# layout nil
-
#
-
# class VaultController < BankController
-
# layout :access_level_layout
-
#
-
# class TillController < BankController
-
# layout false
-
#
-
# In these examples, we have three implicit lookup scenarios:
-
# * The +BankController+ uses the "bank" layout.
-
# * The +ExchangeController+ uses the "exchange" layout.
-
# * The +CurrencyController+ inherits the layout from BankController.
-
#
-
# However, when a layout is explicitly set, the explicitly set layout wins:
-
# * The +InformationController+ uses the "information" layout, explicitly set.
-
# * The +TellerController+ also uses the "information" layout, because the parent explicitly set it.
-
# * The +EmployeeController+ uses the "employee" layout, because it set the layout to +nil+, resetting the parent configuration.
-
# * The +VaultController+ chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
-
# * The +TillController+ does not use a layout at all.
-
#
-
# == Types of layouts
-
#
-
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
-
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
-
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
-
#
-
# The method reference is the preferred approach to variable layouts and is used like this:
-
#
-
# class WeblogController < ActionController::Base
-
# layout :writers_and_readers
-
#
-
# def index
-
# # fetching posts
-
# end
-
#
-
# private
-
# def writers_and_readers
-
# logged_in? ? "writer_layout" : "reader_layout"
-
# end
-
# end
-
#
-
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
-
# is logged in or not.
-
#
-
# If you want to use an inline method, such as a proc, do something like this:
-
#
-
# class WeblogController < ActionController::Base
-
# layout proc { |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
-
# end
-
#
-
# If an argument isn't given to the proc, it's evaluated in the context of
-
# the current controller anyway.
-
#
-
# class WeblogController < ActionController::Base
-
# layout proc { logged_in? ? "writer_layout" : "reader_layout" }
-
# end
-
#
-
# Of course, the most common way of specifying a layout is still just as a plain template name:
-
#
-
# class WeblogController < ActionController::Base
-
# layout "weblog_standard"
-
# end
-
#
-
# The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
-
# <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
-
#
-
# Setting the layout to +nil+ forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
-
# Setting it to +nil+ is useful to re-enable template lookup overriding a previous configuration set in the parent:
-
#
-
# class ApplicationController < ActionController::Base
-
# layout "application"
-
# end
-
#
-
# class PostsController < ApplicationController
-
# # Will use "application" layout
-
# end
-
#
-
# class CommentsController < ApplicationController
-
# # Will search for "comments" layout and fallback "application" layout
-
# layout nil
-
# end
-
#
-
# == Conditional layouts
-
#
-
# If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
-
# a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
-
# <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
-
#
-
# class WeblogController < ActionController::Base
-
# layout "weblog_standard", except: :rss
-
#
-
# # ...
-
#
-
# end
-
#
-
# This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will
-
# be rendered directly, without wrapping a layout around the rendered view.
-
#
-
# Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
-
# #<tt>except: [ :rss, :text_only ]</tt> is valid, as is <tt>except: :rss</tt>.
-
#
-
# == Using a different layout in the action render call
-
#
-
# If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
-
# Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
-
# You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
-
#
-
# class WeblogController < ActionController::Base
-
# layout "weblog_standard"
-
#
-
# def help
-
# render action: "help", layout: "help"
-
# end
-
# end
-
#
-
# This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead.
-
1
module Layouts
-
1
extend ActiveSupport::Concern
-
-
1
include ActionView::Rendering
-
-
1
included do
-
2
class_attribute :_layout, instance_accessor: false
-
2
class_attribute :_layout_conditions, instance_accessor: false, default: {}
-
-
2
_write_layout_method
-
end
-
-
1
delegate :_layout_conditions, to: :class
-
-
1
module ClassMethods
-
1
def inherited(klass) # :nodoc:
-
1
super
-
1
klass._write_layout_method
-
end
-
-
# This module is mixed in if layout conditions are provided. This means
-
# that if no layout conditions are used, this method is not used
-
1
module LayoutConditions # :nodoc:
-
1
private
-
# Determines whether the current action has a layout definition by
-
# checking the action name against the :only and :except conditions
-
# set by the <tt>layout</tt> method.
-
#
-
# ==== Returns
-
# * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise.
-
1
def _conditional_layout?
-
return unless super
-
-
conditions = _layout_conditions
-
-
if only = conditions[:only]
-
only.include?(action_name)
-
elsif except = conditions[:except]
-
!except.include?(action_name)
-
else
-
true
-
end
-
end
-
end
-
-
# Specify the layout to use for this class.
-
#
-
# If the specified layout is a:
-
# String:: the String is the template name
-
# Symbol:: call the method specified by the symbol
-
# Proc:: call the passed Proc
-
# false:: There is no layout
-
# true:: raise an ArgumentError
-
# nil:: Force default layout behavior with inheritance
-
#
-
# Return value of +Proc+ and +Symbol+ arguments should be +String+, +false+, +true+ or +nil+
-
# with the same meaning as described above.
-
# ==== Parameters
-
# * <tt>layout</tt> - The layout to use.
-
#
-
# ==== Options (conditions)
-
# * :only - A list of actions to apply this layout to.
-
# * :except - Apply this layout to all actions but this one.
-
1
def layout(layout, conditions = {})
-
include LayoutConditions unless conditions.empty?
-
-
conditions.each { |k, v| conditions[k] = Array(v).map(&:to_s) }
-
self._layout_conditions = conditions
-
-
self._layout = layout
-
_write_layout_method
-
end
-
-
# Creates a _layout method to be called by _default_layout .
-
#
-
# If a layout is not explicitly mentioned then look for a layout with the controller's name.
-
# if nothing is found then try same procedure to find super class's layout.
-
1
def _write_layout_method # :nodoc:
-
3
silence_redefinition_of_method(:_layout)
-
-
3
prefixes = /\blayouts/.match?(_implied_layout_name) ? [] : ["layouts"]
-
3
default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super"
-
3
name_clause = if name
-
3
default_behavior
-
else
-
<<-RUBY
-
super
-
RUBY
-
end
-
-
layout_definition = \
-
3
case _layout
-
when String
-
_layout.inspect
-
when Symbol
-
<<-RUBY
-
#{_layout}.tap do |layout|
-
return #{default_behavior} if layout.nil?
-
unless layout.is_a?(String) || !layout
-
raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \
-
"should have returned a String, false, or nil"
-
end
-
end
-
RUBY
-
when Proc
-
define_method :_layout_from_proc, &_layout
-
private :_layout_from_proc
-
<<-RUBY
-
result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'})
-
return #{default_behavior} if result.nil?
-
result
-
RUBY
-
when false
-
nil
-
when true
-
raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil"
-
when nil
-
3
name_clause
-
end
-
-
3
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
# frozen_string_literal: true
-
def _layout(lookup_context, formats)
-
if _conditional_layout?
-
#{layout_definition}
-
else
-
#{name_clause}
-
end
-
end
-
private :_layout
-
RUBY
-
end
-
-
1
private
-
# If no layout is supplied, look for a template named the return
-
# value of this method.
-
#
-
# ==== Returns
-
# * <tt>String</tt> - A template name
-
1
def _implied_layout_name
-
6
controller_path
-
end
-
end
-
-
1
def _normalize_options(options) # :nodoc:
-
super
-
-
if _include_layout?(options)
-
layout = options.delete(:layout) { :default }
-
options[:layout] = _layout_for_option(layout)
-
end
-
end
-
-
1
attr_internal_writer :action_has_layout
-
-
1
def initialize(*) # :nodoc:
-
@_action_has_layout = true
-
super
-
end
-
-
# Controls whether an action should be rendered using a layout.
-
# If you want to disable any <tt>layout</tt> settings for the
-
# current action so that it is rendered without a layout then
-
# either override this method in your controller to return false
-
# for that action or set the <tt>action_has_layout</tt> attribute
-
# to false before rendering.
-
1
def action_has_layout?
-
@_action_has_layout
-
end
-
-
1
private
-
1
def _conditional_layout?
-
true
-
end
-
-
# This will be overwritten by _write_layout_method
-
1
def _layout(*); end
-
-
# Determine the layout for a given name, taking into account the name type.
-
#
-
# ==== Parameters
-
# * <tt>name</tt> - The name of the template
-
1
def _layout_for_option(name)
-
case name
-
when String then _normalize_layout(name)
-
when Proc then name
-
when true then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, true) }
-
when :default then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, false) }
-
when false, nil then nil
-
else
-
raise ArgumentError,
-
"String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}"
-
end
-
end
-
-
1
def _normalize_layout(value)
-
value.is_a?(String) && !value.match?(/\blayouts/) ? "layouts/#{value}" : value
-
end
-
-
# Returns the default layout for this controller.
-
# Optionally raises an exception if the layout could not be found.
-
#
-
# ==== Parameters
-
# * <tt>formats</tt> - The formats accepted to this layout
-
# * <tt>require_layout</tt> - If set to +true+ and layout is not found,
-
# an +ArgumentError+ exception is raised (defaults to +false+)
-
#
-
# ==== Returns
-
# * <tt>template</tt> - The template object for the default layout (or +nil+)
-
1
def _default_layout(lookup_context, formats, require_layout = false)
-
begin
-
value = _layout(lookup_context, formats) if action_has_layout?
-
rescue NameError => e
-
raise e, "Could not render layout: #{e.message}"
-
end
-
-
if require_layout && action_has_layout? && !value
-
raise ArgumentError,
-
"There was no default layout for #{self.class} in #{view_paths.inspect}"
-
end
-
-
_normalize_layout(value)
-
end
-
-
1
def _include_layout?(options)
-
(options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView #:nodoc:
-
# = Action View PathSet
-
#
-
# This class is used to store and access paths in Action View. A number of
-
# operations are defined so that you can search among the paths in this
-
# set and also perform operations on other +PathSet+ objects.
-
#
-
# A +LookupContext+ will use a +PathSet+ to store the paths in its context.
-
1
class PathSet #:nodoc:
-
1
include Enumerable
-
-
1
attr_reader :paths
-
-
1
delegate :[], :include?, :pop, :size, :each, to: :paths
-
-
1
def initialize(paths = [])
-
8
@paths = typecast paths
-
end
-
-
1
def initialize_copy(other)
-
@paths = other.paths.dup
-
self
-
end
-
-
1
def to_ary
-
6
paths.dup
-
end
-
-
1
def compact
-
PathSet.new paths.compact
-
end
-
-
1
def +(array)
-
PathSet.new(paths + array)
-
end
-
-
1
%w(<< concat push insert unshift).each do |method|
-
5
class_eval <<-METHOD, __FILE__, __LINE__ + 1
-
def #{method}(*args)
-
paths.#{method}(*typecast(args))
-
end
-
METHOD
-
end
-
-
1
def find(*args)
-
find_all(*args).first || raise(MissingTemplate.new(self, *args))
-
end
-
-
1
def find_all(path, prefixes = [], *args)
-
_find_all path, prefixes, args
-
end
-
-
1
def exists?(path, prefixes, *args)
-
find_all(path, prefixes, *args).any?
-
end
-
-
1
def find_all_with_query(query) # :nodoc:
-
paths.each do |resolver|
-
templates = resolver.find_all_with_query(query)
-
return templates unless templates.empty?
-
end
-
-
[]
-
end
-
-
1
private
-
1
def _find_all(path, prefixes, args)
-
prefixes = [prefixes] if String === prefixes
-
prefixes.each do |prefix|
-
paths.each do |resolver|
-
templates = resolver.find_all(path, prefix, *args)
-
return templates unless templates.empty?
-
end
-
end
-
[]
-
end
-
-
1
def typecast(paths)
-
8
paths.map do |path|
-
12
case path
-
when Pathname, String
-
6
OptimizedFileSystemResolver.new path.to_s
-
else
-
6
path
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_view/view_paths"
-
-
1
module ActionView
-
# This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
-
# it will trigger the lookup_context and consequently expire the cache.
-
1
class I18nProxy < ::I18n::Config #:nodoc:
-
1
attr_reader :original_config, :lookup_context
-
-
1
def initialize(original_config, lookup_context)
-
original_config = original_config.original_config if original_config.respond_to?(:original_config)
-
@original_config, @lookup_context = original_config, lookup_context
-
end
-
-
1
def locale
-
@original_config.locale
-
end
-
-
1
def locale=(value)
-
@lookup_context.locale = value
-
end
-
end
-
-
1
module Rendering
-
1
extend ActiveSupport::Concern
-
1
include ActionView::ViewPaths
-
-
1
attr_reader :rendered_format
-
-
1
def initialize
-
@rendered_format = nil
-
super
-
end
-
-
# Overwrite process to set up I18n proxy.
-
1
def process(*) #:nodoc:
-
old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
-
super
-
ensure
-
I18n.config = old_config
-
end
-
-
1
module ClassMethods
-
1
def _routes
-
end
-
-
1
def _helpers
-
end
-
-
1
def build_view_context_class(klass, supports_path, routes, helpers)
-
Class.new(klass) do
-
if routes
-
include routes.url_helpers(supports_path)
-
include routes.mounted_helpers
-
end
-
-
if helpers
-
include helpers
-
end
-
end
-
end
-
-
1
def view_context_class
-
klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
-
-
@view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
-
-
if klass.changed?(@view_context_class)
-
@view_context_class = build_view_context_class(klass, supports_path?, _routes, _helpers)
-
end
-
-
@view_context_class
-
end
-
end
-
-
1
def view_context_class
-
self.class.view_context_class
-
end
-
-
# An instance of a view class. The default view class is ActionView::Base.
-
#
-
# The view class must have the following methods:
-
#
-
# * <tt>View.new(lookup_context, assigns, controller)</tt> — Create a new
-
# ActionView instance for a controller and we can also pass the arguments.
-
#
-
# * <tt>View#render(option)</tt> — Returns String with the rendered template.
-
#
-
# Override this method in a module to change the default behavior.
-
1
def view_context
-
view_context_class.new(lookup_context, view_assigns, self)
-
end
-
-
# Returns an object that is able to render templates.
-
1
def view_renderer # :nodoc:
-
# Lifespan: Per controller
-
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
-
end
-
-
1
def render_to_body(options = {})
-
_process_options(options)
-
_render_template(options)
-
end
-
-
1
private
-
# Find and render a template based on the options given.
-
1
def _render_template(options)
-
variant = options.delete(:variant)
-
assigns = options.delete(:assigns)
-
context = view_context
-
-
context.assign assigns if assigns
-
lookup_context.variants = variant if variant
-
-
rendered_template = context.in_rendering_context(options) do |renderer|
-
renderer.render_to_object(context, options)
-
end
-
-
rendered_format = rendered_template.format || lookup_context.formats.first
-
@rendered_format = Template::Types[rendered_format]
-
-
rendered_template.body
-
end
-
-
# Assign the rendered format to look up context.
-
1
def _process_format(format)
-
super
-
lookup_context.formats = [format.to_sym] if format.to_sym
-
end
-
-
# Normalize args by converting render "foo" to render :action => "foo" and
-
# render "foo/bar" to render :template => "foo/bar".
-
1
def _normalize_args(action = nil, options = {})
-
options = super(action, options)
-
case action
-
when NilClass
-
when Hash
-
options = action
-
when String, Symbol
-
action = action.to_s
-
key = action.include?(?/) ? :template : :action
-
options[key] = action
-
else
-
if action.respond_to?(:permitted?) && action.permitted?
-
options = action
-
elsif action.respond_to?(:render_in)
-
options[:renderable] = action
-
else
-
options[:partial] = action
-
end
-
end
-
-
options
-
end
-
-
# Normalize options.
-
1
def _normalize_options(options)
-
options = super(options)
-
if options[:partial] == true
-
options[:partial] = action_name
-
end
-
-
if (options.keys & [:partial, :file, :template]).empty?
-
options[:prefixes] ||= _prefixes
-
end
-
-
options[:template] ||= (options[:action] || action_name).to_s
-
options
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/routing/polymorphic_routes"
-
-
1
module ActionView
-
1
module RoutingUrlFor
-
# Returns the URL for the set of +options+ provided. This takes the
-
# same options as +url_for+ in Action Controller (see the
-
# documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
-
# <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
-
# instead of the fully qualified URL like "http://example.com/controller/action".
-
#
-
# ==== Options
-
# * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
-
# * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
-
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
-
# is currently not recommended since it breaks caching.
-
# * <tt>:host</tt> - Overrides the default (current) host if provided.
-
# * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
-
# * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
-
# * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
-
#
-
# ==== Relying on named routes
-
#
-
# Passing a record (like an Active Record) instead of a hash as the options parameter will
-
# trigger the named route for that record. The lookup will happen on the name of the class. So passing a
-
# Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
-
# +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
-
#
-
# ==== Implicit Controller Namespacing
-
#
-
# Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one.
-
#
-
# ==== Examples
-
# <%= url_for(action: 'index') %>
-
# # => /blogs/
-
#
-
# <%= url_for(action: 'find', controller: 'books') %>
-
# # => /books/find
-
#
-
# <%= url_for(action: 'login', controller: 'members', only_path: false, protocol: 'https') %>
-
# # => https://www.example.com/members/login/
-
#
-
# <%= url_for(action: 'play', anchor: 'player') %>
-
# # => /messages/play/#player
-
#
-
# <%= url_for(action: 'jump', anchor: 'tax&ship') %>
-
# # => /testing/jump/#tax&ship
-
#
-
# <%= url_for(Workshop.new) %>
-
# # relies on Workshop answering a persisted? call (and in this case returning false)
-
# # => /workshops
-
#
-
# <%= url_for(@workshop) %>
-
# # calls @workshop.to_param which by default returns the id
-
# # => /workshops/5
-
#
-
# # to_param can be re-defined in a model to provide different URL names:
-
# # => /workshops/1-workshop-name
-
#
-
# <%= url_for("http://www.example.com") %>
-
# # => http://www.example.com
-
#
-
# <%= url_for(:back) %>
-
# # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
-
# # => http://www.example.com
-
#
-
# <%= url_for(:back) %>
-
# # if request.env["HTTP_REFERER"] is not set or is blank
-
# # => javascript:history.back()
-
#
-
# <%= url_for(action: 'index', controller: 'users') %>
-
# # Assuming an "admin" namespace
-
# # => /admin/users
-
#
-
# <%= url_for(action: 'index', controller: '/users') %>
-
# # Specify absolute path with beginning slash
-
# # => /users
-
1
def url_for(options = nil)
-
case options
-
when String
-
options
-
when nil
-
super(only_path: _generate_paths_by_default)
-
when Hash
-
options = options.symbolize_keys
-
ensure_only_path_option(options)
-
-
super(options)
-
when ActionController::Parameters
-
ensure_only_path_option(options)
-
-
super(options)
-
when :back
-
_back_url
-
when Array
-
components = options.dup
-
options = components.extract_options!
-
ensure_only_path_option(options)
-
-
if options[:only_path]
-
polymorphic_path(components, options)
-
else
-
polymorphic_url(components, options)
-
end
-
else
-
method = _generate_paths_by_default ? :path : :url
-
builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.public_send(method)
-
-
case options
-
when Symbol
-
builder.handle_string_call(self, options)
-
when Class
-
builder.handle_class_call(self, options)
-
else
-
builder.handle_model_call(self, options)
-
end
-
end
-
end
-
-
1
def url_options #:nodoc:
-
return super unless controller.respond_to?(:url_options)
-
controller.url_options
-
end
-
-
1
private
-
1
def _routes_context
-
controller
-
end
-
-
1
def optimize_routes_generation?
-
controller.respond_to?(:optimize_routes_generation?, true) ?
-
controller.optimize_routes_generation? : super
-
end
-
-
1
def _generate_paths_by_default
-
true
-
end
-
-
1
def ensure_only_path_option(options)
-
unless options.key?(:only_path)
-
options[:only_path] = _generate_paths_by_default unless options[:host]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "thread"
-
1
require "delegate"
-
-
1
module ActionView
-
# = Action View Template
-
1
class Template
-
1
extend ActiveSupport::Autoload
-
-
# === Encodings in ActionView::Template
-
#
-
# ActionView::Template is one of a few sources of potential
-
# encoding issues in Rails. This is because the source for
-
# templates are usually read from disk, and Ruby (like most
-
# encoding-aware programming languages) assumes that the
-
# String retrieved through File IO is encoded in the
-
# <tt>default_external</tt> encoding. In Rails, the default
-
# <tt>default_external</tt> encoding is UTF-8.
-
#
-
# As a result, if a user saves their template as ISO-8859-1
-
# (for instance, using a non-Unicode-aware text editor),
-
# and uses characters outside of the ASCII range, their
-
# users will see diamonds with question marks in them in
-
# the browser.
-
#
-
# For the rest of this documentation, when we say "UTF-8",
-
# we mean "UTF-8 or whatever the default_internal encoding
-
# is set to". By default, it will be UTF-8.
-
#
-
# To mitigate this problem, we use a few strategies:
-
# 1. If the source is not valid UTF-8, we raise an exception
-
# when the template is compiled to alert the user
-
# to the problem.
-
# 2. The user can specify the encoding using Ruby-style
-
# encoding comments in any template engine. If such
-
# a comment is supplied, Rails will apply that encoding
-
# to the resulting compiled source returned by the
-
# template handler.
-
# 3. In all cases, we transcode the resulting String to
-
# the UTF-8.
-
#
-
# This means that other parts of Rails can always assume
-
# that templates are encoded in UTF-8, even if the original
-
# source of the template was not UTF-8.
-
#
-
# From a user's perspective, the easiest thing to do is
-
# to save your templates as UTF-8. If you do this, you
-
# do not need to do anything else for things to "just work".
-
#
-
# === Instructions for template handlers
-
#
-
# The easiest thing for you to do is to simply ignore
-
# encodings. Rails will hand you the template source
-
# as the default_internal (generally UTF-8), raising
-
# an exception for the user before sending the template
-
# to you if it could not determine the original encoding.
-
#
-
# For the greatest simplicity, you can support only
-
# UTF-8 as the <tt>default_internal</tt>. This means
-
# that from the perspective of your handler, the
-
# entire pipeline is just UTF-8.
-
#
-
# === Advanced: Handlers with alternate metadata sources
-
#
-
# If you want to provide an alternate mechanism for
-
# specifying encodings (like ERB does via <%# encoding: ... %>),
-
# you may indicate that you will handle encodings yourself
-
# by implementing <tt>handles_encoding?</tt> on your handler.
-
#
-
# If you do, Rails will not try to encode the String
-
# into the default_internal, passing you the unaltered
-
# bytes tagged with the assumed encoding (from
-
# default_external).
-
#
-
# In this case, make sure you return a String from
-
# your handler encoded in the default_internal. Since
-
# you are handling out-of-band metadata, you are
-
# also responsible for alerting the user to any
-
# problems with converting the user's data to
-
# the <tt>default_internal</tt>.
-
#
-
# To do so, simply raise +WrongEncodingError+ as follows:
-
#
-
# raise WrongEncodingError.new(
-
# problematic_string,
-
# expected_encoding
-
# )
-
-
##
-
# :method: local_assigns
-
#
-
# Returns a hash with the defined local variables.
-
#
-
# Given this sub template rendering:
-
#
-
# <%= render "shared/header", { headline: "Welcome", person: person } %>
-
#
-
# You can use +local_assigns+ in the sub templates to access the local variables:
-
#
-
# local_assigns[:headline] # => "Welcome"
-
-
1
eager_autoload do
-
1
autoload :Error
-
1
autoload :RawFile
-
1
autoload :Renderable
-
1
autoload :Handlers
-
1
autoload :HTML
-
1
autoload :Inline
-
1
autoload :Sources
-
1
autoload :Text
-
1
autoload :Types
-
end
-
-
1
extend Template::Handlers
-
-
1
attr_reader :identifier, :handler
-
1
attr_reader :variable, :format, :variant, :locals, :virtual_path
-
-
1
def initialize(source, identifier, handler, locals:, format: nil, variant: nil, virtual_path: nil)
-
@source = source
-
@identifier = identifier
-
@handler = handler
-
@compiled = false
-
@locals = locals
-
@virtual_path = virtual_path
-
-
@variable = if @virtual_path
-
base = @virtual_path.end_with?("/") ? "" : ::File.basename(@virtual_path)
-
base =~ /\A_?(.*?)(?:\.\w+)*\z/
-
$1.to_sym
-
end
-
-
@format = format
-
@variant = variant
-
@compile_mutex = Mutex.new
-
end
-
-
# Returns whether the underlying handler supports streaming. If so,
-
# a streaming buffer *may* be passed when it starts rendering.
-
1
def supports_streaming?
-
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
-
end
-
-
# Render a template. If the template was not compiled yet, it is done
-
# exactly before rendering.
-
#
-
# This method is instrumented as "!render_template.action_view". Notice that
-
# we use a bang in this instrumentation because you don't want to
-
# consume this in production. This is only slow if it's being listened to.
-
1
def render(view, locals, buffer = ActionView::OutputBuffer.new, add_to_stack: true, &block)
-
instrument_render_template do
-
compile!(view)
-
view._run(method_name, self, locals, buffer, add_to_stack: add_to_stack, &block)
-
end
-
rescue => e
-
handle_render_error(view, e)
-
end
-
-
1
def type
-
@type ||= Types[format]
-
end
-
-
1
def short_identifier
-
@short_identifier ||= defined?(Rails.root) ? identifier.delete_prefix("#{Rails.root}/") : identifier
-
end
-
-
1
def inspect
-
"#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
-
end
-
-
1
def source
-
@source.to_s
-
end
-
-
# This method is responsible for properly setting the encoding of the
-
# source. Until this point, we assume that the source is BINARY data.
-
# If no additional information is supplied, we assume the encoding is
-
# the same as <tt>Encoding.default_external</tt>.
-
#
-
# The user can also specify the encoding via a comment on the first
-
# line of the template (# encoding: NAME-OF-ENCODING). This will work
-
# with any template engine, as we process out the encoding comment
-
# before passing the source on to the template engine, leaving a
-
# blank line in its stead.
-
1
def encode!
-
source = self.source
-
-
return source unless source.encoding == Encoding::BINARY
-
-
# Look for # encoding: *. If we find one, we'll encode the
-
# String in that encoding, otherwise, we'll use the
-
# default external encoding.
-
if source.sub!(/\A#{ENCODING_FLAG}/, "")
-
encoding = magic_encoding = $1
-
else
-
encoding = Encoding.default_external
-
end
-
-
# Tag the source with the default external encoding
-
# or the encoding specified in the file
-
source.force_encoding(encoding)
-
-
# If the user didn't specify an encoding, and the handler
-
# handles encodings, we simply pass the String as is to
-
# the handler (with the default_external tag)
-
if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding?
-
source
-
# Otherwise, if the String is valid in the encoding,
-
# encode immediately to default_internal. This means
-
# that if a handler doesn't handle encodings, it will
-
# always get Strings in the default_internal
-
elsif source.valid_encoding?
-
source.encode!
-
# Otherwise, since the String is invalid in the encoding
-
# specified, raise an exception
-
else
-
raise WrongEncodingError.new(source, encoding)
-
end
-
end
-
-
-
# Exceptions are marshalled when using the parallel test runner with DRb, so we need
-
# to ensure that references to the template object can be marshalled as well. This means forgoing
-
# the marshalling of the compiler mutex and instantiating that again on unmarshalling.
-
1
def marshal_dump # :nodoc:
-
[ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant ]
-
end
-
-
1
def marshal_load(array) # :nodoc:
-
@source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant = *array
-
@compile_mutex = Mutex.new
-
end
-
-
1
private
-
# Compile a template. This method ensures a template is compiled
-
# just once and removes the source after it is compiled.
-
1
def compile!(view)
-
return if @compiled
-
-
# Templates can be used concurrently in threaded environments
-
# so compilation and any instance variable modification must
-
# be synchronized
-
@compile_mutex.synchronize do
-
# Any thread holding this lock will be compiling the template needed
-
# by the threads waiting. So re-check the @compiled flag to avoid
-
# re-compilation
-
return if @compiled
-
-
mod = view.compiled_method_container
-
-
instrument("!compile_template") do
-
compile(mod)
-
end
-
-
@compiled = true
-
end
-
end
-
-
# Among other things, this method is responsible for properly setting
-
# the encoding of the compiled template.
-
#
-
# If the template engine handles encodings, we send the encoded
-
# String to the engine without further processing. This allows
-
# the template engine to support additional mechanisms for
-
# specifying the encoding. For instance, ERB supports <%# encoding: %>
-
#
-
# Otherwise, after we figure out the correct encoding, we then
-
# encode the source into <tt>Encoding.default_internal</tt>.
-
# In general, this means that templates will be UTF-8 inside of Rails,
-
# regardless of the original source encoding.
-
1
def compile(mod)
-
source = encode!
-
code = @handler.call(self, source)
-
-
# Make sure that the resulting String to be eval'd is in the
-
# encoding of the code
-
original_source = source
-
source = +<<-end_src
-
def #{method_name}(local_assigns, output_buffer)
-
@virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
-
end
-
end_src
-
-
# Make sure the source is in the encoding of the returned code
-
source.force_encoding(code.encoding)
-
-
# In case we get back a String from a handler that is not in
-
# BINARY or the default_internal, encode it to the default_internal
-
source.encode!
-
-
# Now, validate that the source we got back from the template
-
# handler is valid in the default_internal. This is for handlers
-
# that handle encoding but screw up
-
unless source.valid_encoding?
-
raise WrongEncodingError.new(source, Encoding.default_internal)
-
end
-
-
begin
-
mod.module_eval(source, identifier, 0)
-
rescue SyntaxError
-
# Account for when code in the template is not syntactically valid; e.g. if we're using
-
# ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate
-
# the result into the template, but missing an end parenthesis.
-
raise SyntaxErrorInTemplate.new(self, original_source)
-
end
-
end
-
-
1
def handle_render_error(view, e)
-
if e.is_a?(Template::Error)
-
e.sub_template_of(self)
-
raise e
-
else
-
raise Template::Error.new(self)
-
end
-
end
-
-
1
def locals_code
-
# Only locals with valid variable names get set directly. Others will
-
# still be available in local_assigns.
-
locals = @locals - Module::RUBY_RESERVED_KEYWORDS
-
locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
-
-
# Assign for the same variable is to suppress unused variable warning
-
locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" }
-
end
-
-
1
def method_name
-
@method_name ||= begin
-
m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
-
m.tr!("-", "_")
-
m
-
end
-
end
-
-
1
def identifier_method_name
-
short_identifier.tr("^a-z_", "_")
-
end
-
-
1
def instrument(action, &block) # :doc:
-
ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block)
-
end
-
-
1
def instrument_render_template(&block)
-
ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block)
-
end
-
-
1
def instrument_payload
-
{ virtual_path: @virtual_path, identifier: @identifier }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView #:nodoc:
-
# = Action View Template Handlers
-
1
class Template #:nodoc:
-
1
module Handlers #:nodoc:
-
1
autoload :Raw, "action_view/template/handlers/raw"
-
1
autoload :ERB, "action_view/template/handlers/erb"
-
1
autoload :Html, "action_view/template/handlers/html"
-
1
autoload :Builder, "action_view/template/handlers/builder"
-
-
1
def self.extended(base)
-
1
base.register_default_template_handler :raw, Raw.new
-
1
base.register_template_handler :erb, ERB.new
-
1
base.register_template_handler :html, Html.new
-
1
base.register_template_handler :builder, Builder.new
-
1
base.register_template_handler :ruby, lambda { |_, source| source }
-
end
-
-
1
@@template_handlers = {}
-
1
@@default_template_handlers = nil
-
-
1
def self.extensions
-
@@template_extensions ||= @@template_handlers.keys
-
end
-
-
# Register an object that knows how to handle template files with the given
-
# extensions. This can be used to implement new template types.
-
# The handler must respond to +:call+, which will be passed the template
-
# and should return the rendered template as a String.
-
1
def register_template_handler(*extensions, handler)
-
5
raise(ArgumentError, "Extension is required") if extensions.empty?
-
5
extensions.each do |extension|
-
5
@@template_handlers[extension.to_sym] = handler
-
end
-
5
@@template_extensions = nil
-
end
-
-
# Opposite to register_template_handler.
-
1
def unregister_template_handler(*extensions)
-
extensions.each do |extension|
-
handler = @@template_handlers.delete extension.to_sym
-
@@default_template_handlers = nil if @@default_template_handlers == handler
-
end
-
@@template_extensions = nil
-
end
-
-
1
def template_handler_extensions
-
@@template_handlers.keys.map(&:to_s).sort
-
end
-
-
1
def registered_template_handler(extension)
-
extension && @@template_handlers[extension.to_sym]
-
end
-
-
1
def register_default_template_handler(extension, klass)
-
1
register_template_handler(extension, klass)
-
1
@@default_template_handlers = klass
-
end
-
-
1
def handler_for_extension(extension)
-
registered_template_handler(extension) || @@default_template_handlers
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module Template::Handlers
-
1
class Builder
-
1
class_attribute :default_format, default: :xml
-
-
1
def call(template, source)
-
require_engine
-
"xml = ::Builder::XmlMarkup.new(:indent => 2);" \
-
"self.output_buffer = xml.target!;" +
-
source +
-
";xml.target!;"
-
end
-
-
1
private
-
1
def require_engine # :doc:
-
@required ||= begin
-
require "builder"
-
true
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
class Template
-
1
module Handlers
-
1
class ERB
-
1
autoload :Erubi, "action_view/template/handlers/erb/erubi"
-
-
# Specify trim mode for the ERB compiler. Defaults to '-'.
-
# See ERB documentation for suitable values.
-
1
class_attribute :erb_trim_mode, default: "-"
-
-
# Default implementation used.
-
1
class_attribute :erb_implementation, default: Erubi
-
-
# Do not escape templates of these mime types.
-
1
class_attribute :escape_ignore_list, default: ["text/plain"]
-
-
1
ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
-
-
1
def self.call(template, source)
-
new.call(template, source)
-
end
-
-
1
def supports_streaming?
-
true
-
end
-
-
1
def handles_encoding?
-
true
-
end
-
-
1
def call(template, source)
-
# First, convert to BINARY, so in case the encoding is
-
# wrong, we can still find an encoding tag
-
# (<%# encoding %>) inside the String using a regular
-
# expression
-
template_source = source.b
-
-
erb = template_source.gsub(ENCODING_TAG, "")
-
encoding = $2
-
-
erb.force_encoding valid_encoding(source.dup, encoding)
-
-
# Always make sure we return a String in the default_internal
-
erb.encode!
-
-
options = {
-
escape: (self.class.escape_ignore_list.include? template.type),
-
trim: (self.class.erb_trim_mode == "-")
-
}
-
-
if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
-
options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier} -->';"
-
options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer.to_s"
-
end
-
-
self.class.erb_implementation.new(erb, options).src
-
end
-
-
1
private
-
1
def valid_encoding(string, encoding)
-
# If a magic encoding comment was found, tag the
-
# String with this encoding. This is for a case
-
# where the original String was assumed to be,
-
# for instance, UTF-8, but a magic comment
-
# proved otherwise
-
string.force_encoding(encoding) if encoding
-
-
# If the String is valid, return the encoding we found
-
return string.encoding if string.valid_encoding?
-
-
# Otherwise, raise an exception
-
raise WrongEncodingError.new(string, string.encoding)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "erubi"
-
-
1
module ActionView
-
1
class Template
-
1
module Handlers
-
1
class ERB
-
1
class Erubi < ::Erubi::Engine
-
# :nodoc: all
-
1
def initialize(input, properties = {})
-
@newline_pending = 0
-
-
# Dup properties so that we don't modify argument
-
properties = Hash[properties]
-
-
properties[:bufvar] ||= "@output_buffer"
-
properties[:preamble] ||= ""
-
properties[:postamble] ||= "#{properties[:bufvar]}.to_s"
-
-
properties[:escapefunc] = ""
-
-
super
-
end
-
-
1
def evaluate(action_view_erb_handler_context)
-
src = @src
-
view = Class.new(ActionView::Base) {
-
include action_view_erb_handler_context._routes.url_helpers
-
class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
-
}.empty
-
view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
-
end
-
-
1
private
-
1
def add_text(text)
-
return if text.empty?
-
-
if text == "\n"
-
@newline_pending += 1
-
else
-
src << bufvar << ".safe_append='"
-
src << "\n" * @newline_pending if @newline_pending > 0
-
src << text.gsub(/['\\]/, '\\\\\&')
-
src << "'.freeze;"
-
-
@newline_pending = 0
-
end
-
end
-
-
1
BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
-
-
1
def add_expression(indicator, code)
-
flush_newline_if_pending(src)
-
-
if (indicator == "==") || @escape
-
src << bufvar << ".safe_expr_append="
-
else
-
src << bufvar << ".append="
-
end
-
-
if BLOCK_EXPR.match?(code)
-
src << " " << code
-
else
-
src << "(" << code << ");"
-
end
-
end
-
-
1
def add_code(code)
-
flush_newline_if_pending(src)
-
super
-
end
-
-
1
def add_postamble(_)
-
flush_newline_if_pending(src)
-
super
-
end
-
-
1
def flush_newline_if_pending(src)
-
if @newline_pending > 0
-
src << bufvar << ".safe_append='#{"\n" * @newline_pending}'.freeze;"
-
@newline_pending = 0
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module Template::Handlers
-
1
class Html < Raw
-
1
def call(template, source)
-
"ActionView::OutputBuffer.new #{super}"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module Template::Handlers
-
1
class Raw
-
1
def call(template, source)
-
"#{source.inspect}.html_safe;"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "pathname"
-
1
require "active_support/core_ext/class"
-
1
require "active_support/core_ext/module/attribute_accessors"
-
1
require "action_view/template"
-
1
require "thread"
-
1
require "concurrent/map"
-
-
1
module ActionView
-
# = Action View Resolver
-
1
class Resolver
-
# Keeps all information about view path and builds virtual path.
-
1
class Path
-
1
attr_reader :name, :prefix, :partial, :virtual
-
1
alias_method :partial?, :partial
-
-
1
def self.build(name, prefix, partial)
-
virtual = +""
-
virtual << "#{prefix}/" unless prefix.empty?
-
virtual << (partial ? "_#{name}" : name)
-
new name, prefix, partial, virtual
-
end
-
-
1
def initialize(name, prefix, partial, virtual)
-
@name = name
-
@prefix = prefix
-
@partial = partial
-
@virtual = virtual
-
end
-
-
1
def to_str
-
@virtual
-
end
-
1
alias :to_s :to_str
-
end
-
-
1
class PathParser # :nodoc:
-
1
def build_path_regex
-
handlers = Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
-
formats = Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|")
-
locales = "[a-z]{2}(?:-[A-Z]{2})?"
-
variants = "[^.]*"
-
-
%r{
-
\A
-
(?:(?<prefix>.*)/)?
-
(?<partial>_)?
-
(?<action>.*?)
-
(?:\.(?<locale>#{locales}))??
-
(?:\.(?<format>#{formats}))??
-
(?:\+(?<variant>#{variants}))??
-
(?:\.(?<handler>#{handlers}))?
-
\z
-
}x
-
end
-
-
1
def parse(path)
-
@regex ||= build_path_regex
-
match = @regex.match(path)
-
{
-
prefix: match[:prefix] || "",
-
action: match[:action],
-
partial: !!match[:partial],
-
locale: match[:locale]&.to_sym,
-
handler: match[:handler]&.to_sym,
-
format: match[:format]&.to_sym,
-
variant: match[:variant]
-
}
-
end
-
end
-
-
# Threadsafe template cache
-
1
class Cache #:nodoc:
-
1
class SmallCache < Concurrent::Map
-
1
def initialize(options = {})
-
12
super(options.merge(initial_capacity: 2))
-
end
-
end
-
-
# Preallocate all the default blocks for performance/memory consumption reasons
-
1
PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
-
1
PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
-
1
NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
-
1
KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) }
-
-
# Usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
-
1
NO_TEMPLATES = [].freeze
-
-
1
def initialize
-
6
@data = SmallCache.new(&KEY_BLOCK)
-
6
@query_cache = SmallCache.new
-
end
-
-
1
def inspect
-
"#{to_s[0..-2]} keys=#{@data.size} queries=#{@query_cache.size}>"
-
end
-
-
# Cache the templates returned by the block
-
1
def cache(key, name, prefix, partial, locals)
-
@data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
-
end
-
-
1
def cache_query(query) # :nodoc:
-
@query_cache[query] ||= canonical_no_templates(yield)
-
end
-
-
1
def clear
-
@data.clear
-
@query_cache.clear
-
end
-
-
# Get the cache size. Do not call this
-
# method. This method is not guaranteed to be here ever.
-
1
def size # :nodoc:
-
size = 0
-
@data.each_value do |v1|
-
v1.each_value do |v2|
-
v2.each_value do |v3|
-
v3.each_value do |v4|
-
size += v4.size
-
end
-
end
-
end
-
end
-
-
size + @query_cache.size
-
end
-
-
1
private
-
1
def canonical_no_templates(templates)
-
templates.empty? ? NO_TEMPLATES : templates
-
end
-
end
-
-
1
cattr_accessor :caching, default: true
-
-
1
class << self
-
1
alias :caching? :caching
-
end
-
-
1
def initialize
-
6
@cache = Cache.new
-
end
-
-
1
def clear_cache
-
@cache.clear
-
end
-
-
# Normalizes the arguments and passes it on to find_templates.
-
1
def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
-
locals = locals.map(&:to_s).sort!.freeze
-
-
cached(key, [name, prefix, partial], details, locals) do
-
_find_all(name, prefix, partial, details, key, locals)
-
end
-
end
-
-
1
def find_all_with_query(query) # :nodoc:
-
@cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
-
end
-
-
1
private
-
1
def _find_all(name, prefix, partial, details, key, locals)
-
find_templates(name, prefix, partial, details, locals)
-
end
-
-
1
delegate :caching?, to: :class
-
-
# This is what child classes implement. No defaults are needed
-
# because Resolver guarantees that the arguments are present and
-
# normalized.
-
1
def find_templates(name, prefix, partial, details, locals = [])
-
raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
-
end
-
-
# Handles templates caching. If a key is given and caching is on
-
# always check the cache before hitting the resolver. Otherwise,
-
# it always hits the resolver but if the key is present, check if the
-
# resolver is fresher before returning it.
-
1
def cached(key, path_info, details, locals)
-
name, prefix, partial = path_info
-
-
if key
-
@cache.cache(key, name, prefix, partial, locals) do
-
yield
-
end
-
else
-
yield
-
end
-
end
-
end
-
-
# An abstract class that implements a Resolver with path semantics.
-
1
class PathResolver < Resolver #:nodoc:
-
1
EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
-
1
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
-
-
1
def initialize
-
6
@pattern = DEFAULT_PATTERN
-
6
@unbound_templates = Concurrent::Map.new
-
6
@path_parser = PathParser.new
-
6
super
-
end
-
-
1
def clear_cache
-
@unbound_templates.clear
-
@path_parser = PathParser.new
-
super
-
end
-
-
1
private
-
1
def _find_all(name, prefix, partial, details, key, locals)
-
path = Path.build(name, prefix, partial)
-
query(path, details, details[:formats], locals, cache: !!key)
-
end
-
-
1
def query(path, details, formats, locals, cache:)
-
template_paths = find_template_paths_from_details(path, details)
-
template_paths = reject_files_external_to_app(template_paths)
-
-
template_paths.map do |template|
-
unbound_template =
-
if cache
-
@unbound_templates.compute_if_absent([template, path.virtual]) do
-
build_unbound_template(template, path.virtual)
-
end
-
else
-
build_unbound_template(template, path.virtual)
-
end
-
-
unbound_template.bind_locals(locals)
-
end
-
end
-
-
1
def source_for_template(template)
-
Template::Sources::File.new(template)
-
end
-
-
1
def build_unbound_template(template, virtual_path)
-
handler, format, variant = extract_handler_and_format_and_variant(template)
-
source = source_for_template(template)
-
-
UnboundTemplate.new(
-
source,
-
template,
-
handler,
-
virtual_path: virtual_path,
-
format: format,
-
variant: variant,
-
)
-
end
-
-
1
def reject_files_external_to_app(files)
-
files.reject { |filename| !inside_path?(@path, filename) }
-
end
-
-
1
def find_template_paths_from_details(path, details)
-
if path.name.include?(".")
-
ActiveSupport::Deprecation.warn("Rendering actions with '.' in the name is deprecated: #{path}")
-
end
-
-
query = build_query(path, details)
-
find_template_paths(query)
-
end
-
-
1
def find_template_paths(query)
-
Dir[query].uniq.reject do |filename|
-
File.directory?(filename) ||
-
# deals with case-insensitive file systems.
-
!File.fnmatch(query, filename, File::FNM_EXTGLOB)
-
end
-
end
-
-
1
def inside_path?(path, filename)
-
filename = File.expand_path(filename)
-
path = File.join(path, "")
-
filename.start_with?(path)
-
end
-
-
# Helper for building query glob string based on resolver's pattern.
-
1
def build_query(path, details)
-
query = @pattern.dup
-
-
prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
-
query.gsub!(/:prefix(\/)?/, prefix)
-
-
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
-
query.gsub!(":action", partial)
-
-
details.each do |ext, candidates|
-
if ext == :variants && candidates == :any
-
query.gsub!(/:#{ext}/, "*")
-
else
-
query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
-
end
-
end
-
-
File.expand_path(query, @path)
-
end
-
-
1
def escape_entry(entry)
-
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
-
end
-
-
# Extract handler, formats and variant from path. If a format cannot be found neither
-
# from the path, or the handler, we should return the array of formats given
-
# to the resolver.
-
1
def extract_handler_and_format_and_variant(path)
-
details = @path_parser.parse(path)
-
-
handler = Template.handler_for_extension(details[:handler])
-
format = details[:format] || handler.try(:default_format)
-
variant = details[:variant]
-
-
# Template::Types[format] and handler.default_format can return nil
-
[handler, format, variant]
-
end
-
end
-
-
# A resolver that loads files from the filesystem.
-
1
class FileSystemResolver < PathResolver
-
1
attr_reader :path
-
-
1
def initialize(path)
-
6
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
-
6
super()
-
6
@path = File.expand_path(path)
-
end
-
-
1
def to_s
-
@path.to_s
-
end
-
1
alias :to_path :to_s
-
-
1
def eql?(resolver)
-
self.class.equal?(resolver.class) && to_path == resolver.to_path
-
end
-
1
alias :== :eql?
-
end
-
-
# An Optimized resolver for Rails' most common case.
-
1
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
-
1
def initialize(path)
-
6
super(path)
-
end
-
-
1
private
-
1
def find_candidate_template_paths(path)
-
# Instead of checking for every possible path, as our other globs would
-
# do, scan the directory for files with the right prefix.
-
query = "#{escape_entry(File.join(@path, path))}*"
-
-
Dir[query].reject do |filename|
-
File.directory?(filename)
-
end
-
end
-
-
1
def find_template_paths_from_details(path, details)
-
if path.name.include?(".")
-
# Fall back to the unoptimized resolver, which will warn
-
return super
-
end
-
-
candidates = find_candidate_template_paths(path)
-
-
regex = build_regex(path, details)
-
-
candidates.uniq.reject do |filename|
-
# This regex match does double duty of finding only files which match
-
# details (instead of just matching the prefix) and also filtering for
-
# case-insensitive file systems.
-
!regex.match?(filename) ||
-
File.directory?(filename)
-
end.sort_by do |filename|
-
# Because we scanned the directory, instead of checking for files
-
# one-by-one, they will be returned in an arbitrary order.
-
# We can use the matches found by the regex and sort by their index in
-
# details.
-
match = filename.match(regex)
-
EXTENSIONS.keys.map do |ext|
-
if ext == :variants && details[ext] == :any
-
match[ext].nil? ? 0 : 1
-
elsif match[ext].nil?
-
# No match should be last
-
details[ext].length
-
else
-
found = match[ext].to_sym
-
details[ext].index(found)
-
end
-
end
-
end
-
end
-
-
1
def build_regex(path, details)
-
query = Regexp.escape(File.join(@path, path))
-
exts = EXTENSIONS.map do |ext, prefix|
-
match =
-
if ext == :variants && details[ext] == :any
-
".*?"
-
else
-
arr = details[ext].compact
-
arr.uniq!
-
arr.map! { |e| Regexp.escape(e) }
-
arr.join("|")
-
end
-
prefix = Regexp.escape(prefix)
-
"(#{prefix}(?<#{ext}>#{match}))?"
-
end.join
-
-
%r{\A#{query}#{exts}\z}
-
end
-
end
-
-
# The same as FileSystemResolver but does not allow templates to store
-
# a virtual path since it is invalid for such resolvers.
-
1
class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
-
1
private_class_method :new
-
-
1
def self.instances
-
[new(""), new("/")]
-
end
-
-
1
def build_unbound_template(template, _)
-
super(template, nil)
-
end
-
-
1
def reject_files_external_to_app(files)
-
files
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/redefine_method"
-
1
require "action_controller"
-
1
require "action_controller/test_case"
-
1
require "action_view"
-
-
1
require "rails-dom-testing"
-
-
1
module ActionView
-
# = Action View Test Case
-
1
class TestCase < ActiveSupport::TestCase
-
1
class TestController < ActionController::Base
-
1
include ActionDispatch::TestProcess
-
-
1
attr_accessor :request, :response, :params
-
-
1
class << self
-
# Overrides AbstractController::Base#controller_path
-
1
attr_accessor :controller_path
-
end
-
-
1
def controller_path=(path)
-
self.class.controller_path = path
-
end
-
-
1
def initialize
-
super
-
self.class.controller_path = ""
-
@request = ActionController::TestRequest.create(self.class)
-
@response = ActionDispatch::TestResponse.new
-
-
@request.env.delete("PATH_INFO")
-
@params = ActionController::Parameters.new
-
end
-
end
-
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
-
1
include ActionDispatch::Assertions, ActionDispatch::TestProcess
-
1
include Rails::Dom::Testing::Assertions
-
1
include ActionController::TemplateAssertions
-
1
include ActionView::Context
-
-
1
include ActionDispatch::Routing::PolymorphicRoutes
-
-
1
include AbstractController::Helpers
-
1
include ActionView::Helpers
-
1
include ActionView::RecordIdentifier
-
1
include ActionView::RoutingUrlFor
-
-
1
include ActiveSupport::Testing::ConstantLookup
-
-
1
delegate :lookup_context, to: :controller
-
1
attr_accessor :controller, :output_buffer, :rendered
-
-
1
module ClassMethods
-
1
def tests(helper_class)
-
case helper_class
-
when String, Symbol
-
self.helper_class = "#{helper_class.to_s.underscore}_helper".camelize.safe_constantize
-
when Module
-
self.helper_class = helper_class
-
end
-
end
-
-
1
def determine_default_helper_class(name)
-
determine_constant_from_test_name(name) do |constant|
-
Module === constant && !(Class === constant)
-
end
-
end
-
-
1
def helper_method(*methods)
-
# Almost a duplicate from ActionController::Helpers
-
methods.flatten.each do |method|
-
_helpers_for_modification.module_eval <<-end_eval, __FILE__, __LINE__ + 1
-
def #{method}(*args, &block) # def current_user(*args, &block)
-
_test_case.send(:'#{method}', *args, &block) # _test_case.send(:'current_user', *args, &block)
-
end # end
-
ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true)
-
end_eval
-
end
-
end
-
-
1
attr_writer :helper_class
-
-
1
def helper_class
-
@helper_class ||= determine_default_helper_class(name)
-
end
-
-
1
def new(*)
-
include_helper_modules!
-
super
-
end
-
-
1
private
-
1
def include_helper_modules!
-
helper(helper_class) if helper_class
-
include _helpers
-
end
-
end
-
-
1
def setup_with_controller
-
controller_class = Class.new(ActionView::TestCase::TestController)
-
@controller = controller_class.new
-
@request = @controller.request
-
@view_flow = ActionView::OutputFlow.new
-
# empty string ensures buffer has UTF-8 encoding as
-
# new without arguments returns ASCII-8BIT encoded buffer like String#new
-
@output_buffer = ActiveSupport::SafeBuffer.new ""
-
@rendered = +""
-
-
test_case_instance = self
-
controller_class.define_method(:_test_case) { test_case_instance }
-
end
-
-
1
def config
-
@controller.config if @controller.respond_to?(:config)
-
end
-
-
1
def render(options = {}, local_assigns = {}, &block)
-
view.assign(view_assigns)
-
@rendered << output = view.render(options, local_assigns, &block)
-
output
-
end
-
-
1
def rendered_views
-
@_rendered_views ||= RenderedViewsCollection.new
-
end
-
-
1
def _routes
-
@controller._routes if @controller.respond_to?(:_routes)
-
end
-
-
# Need to experiment if this priority is the best one: rendered => output_buffer
-
1
class RenderedViewsCollection
-
1
def initialize
-
@rendered_views ||= Hash.new { |hash, key| hash[key] = [] }
-
end
-
-
1
def add(view, locals)
-
@rendered_views[view] ||= []
-
@rendered_views[view] << locals
-
end
-
-
1
def locals_for(view)
-
@rendered_views[view]
-
end
-
-
1
def rendered_views
-
@rendered_views.keys
-
end
-
-
1
def view_rendered?(view, expected_locals)
-
locals_for(view).any? do |actual_locals|
-
expected_locals.all? { |key, value| value == actual_locals[key] }
-
end
-
end
-
end
-
-
1
included do
-
1
setup :setup_with_controller
-
1
ActiveSupport.run_load_hooks(:action_view_test_case, self)
-
-
1
helper do
-
1
def protect_against_forgery?
-
false
-
end
-
-
1
def _test_case
-
controller._test_case
-
end
-
end
-
end
-
-
1
private
-
# Need to experiment if this priority is the best one: rendered => output_buffer
-
1
def document_root_element
-
Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered).root
-
end
-
-
1
module Locals
-
1
attr_accessor :rendered_views
-
-
1
def render(options = {}, local_assigns = {})
-
case options
-
when Hash
-
if block_given?
-
rendered_views.add options[:layout], options[:locals]
-
elsif options.key?(:partial)
-
rendered_views.add options[:partial], options[:locals]
-
end
-
else
-
rendered_views.add options, local_assigns
-
end
-
-
super
-
end
-
end
-
-
# The instance of ActionView::Base that is used by +render+.
-
1
def view
-
@view ||= begin
-
view = @controller.view_context
-
view.singleton_class.include(_helpers)
-
view.extend(Locals)
-
view.rendered_views = rendered_views
-
view.output_buffer = output_buffer
-
view
-
end
-
end
-
-
1
alias_method :_view, :view
-
-
1
INTERNAL_IVARS = [
-
:@NAME,
-
:@failures,
-
:@assertions,
-
:@__io__,
-
:@_assertion_wrapped,
-
:@_assertions,
-
:@_result,
-
:@_routes,
-
:@controller,
-
:@_layouts,
-
:@_files,
-
:@_rendered_views,
-
:@method_name,
-
:@output_buffer,
-
:@_partials,
-
:@passed,
-
:@rendered,
-
:@request,
-
:@routes,
-
:@tagged_logger,
-
:@_templates,
-
:@options,
-
:@test_passed,
-
:@view,
-
:@view_context_class,
-
:@view_flow,
-
:@_subscribers,
-
:@html_document
-
]
-
-
1
def _user_defined_ivars
-
instance_variables - INTERNAL_IVARS
-
end
-
-
# Returns a Hash of instance variables and their values, as defined by
-
# the user in the test case, which are then assigned to the view being
-
# rendered. This is generally intended for internal use and extension
-
# frameworks.
-
1
def view_assigns
-
Hash[_user_defined_ivars.map do |ivar|
-
[ivar[1..-1].to_sym, instance_variable_get(ivar)]
-
end]
-
end
-
-
1
def method_missing(selector, *args)
-
begin
-
routes = @controller.respond_to?(:_routes) && @controller._routes
-
rescue
-
# Don't call routes, if there is an error on _routes call
-
end
-
-
if routes &&
-
(routes.named_routes.route_defined?(selector) ||
-
routes.mounted_helpers.method_defined?(selector))
-
@controller.__send__(selector, *args)
-
else
-
super
-
end
-
end
-
-
1
def respond_to_missing?(name, include_private = false)
-
begin
-
routes = defined?(@controller) && @controller.respond_to?(:_routes) && @controller._routes
-
rescue
-
# Don't call routes, if there is an error on _routes call
-
end
-
-
routes &&
-
(routes.named_routes.route_defined?(name) ||
-
routes.mounted_helpers.method_defined?(name))
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_view/template/resolver"
-
-
1
module ActionView #:nodoc:
-
# Use FixtureResolver in your tests to simulate the presence of files on the
-
# file system. This is used internally by Rails' own test suite, and is
-
# useful for testing extensions that have no way of knowing what the file
-
# system will look like at runtime.
-
1
class FixtureResolver < OptimizedFileSystemResolver
-
1
def initialize(hash = {})
-
super("")
-
@hash = hash
-
@path = ""
-
end
-
-
1
def data
-
@hash
-
end
-
-
1
def to_s
-
@hash.keys.join(", ")
-
end
-
-
1
private
-
1
def find_candidate_template_paths(path)
-
@hash.keys.select do |fixture|
-
fixture.start_with?(path.virtual)
-
end.map do |fixture|
-
"/#{fixture}"
-
end
-
end
-
-
1
def source_for_template(template)
-
@hash[template[1..template.size]]
-
end
-
end
-
-
1
class NullResolver < PathResolver
-
1
def query(path, exts, _, locals, cache:)
-
handler, format, variant = extract_handler_and_format_and_variant(path)
-
[ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant, locals: locals)]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module ViewPaths
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
2
ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze)
-
end
-
-
1
delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
-
:locale, :locale=, to: :lookup_context
-
-
1
module ClassMethods
-
1
def _view_paths
-
6
ViewPaths.get_view_paths(self)
-
end
-
-
1
def _view_paths=(paths)
-
6
ViewPaths.set_view_paths(self, paths)
-
end
-
-
1
def _prefixes # :nodoc:
-
@_prefixes ||= begin
-
return local_prefixes if superclass.abstract?
-
-
local_prefixes + superclass._prefixes
-
end
-
end
-
-
# Append a path to the list of view paths for this controller.
-
#
-
# ==== Parameters
-
# * <tt>path</tt> - If a String is provided, it gets converted into
-
# the default view path. You may also provide a custom view path
-
# (see ActionView::PathSet for more information)
-
1
def append_view_path(path)
-
self._view_paths = view_paths + Array(path)
-
end
-
-
# Prepend a path to the list of view paths for this controller.
-
#
-
# ==== Parameters
-
# * <tt>path</tt> - If a String is provided, it gets converted into
-
# the default view path. You may also provide a custom view path
-
# (see ActionView::PathSet for more information)
-
1
def prepend_view_path(path)
-
6
self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
-
end
-
-
# A list of all of the default view paths for this controller.
-
1
def view_paths
-
6
_view_paths
-
end
-
-
# Set the view paths.
-
#
-
# ==== Parameters
-
# * <tt>paths</tt> - If a PathSet is provided, use that;
-
# otherwise, process the parameter into a PathSet.
-
1
def view_paths=(paths)
-
self._view_paths = ActionView::PathSet.new(Array(paths))
-
end
-
-
1
private
-
# Override this method in your controller if you want to change paths prefixes for finding views.
-
# Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
-
1
def local_prefixes
-
[controller_path]
-
end
-
end
-
-
# :stopdoc:
-
1
@all_view_paths = {}
-
-
1
def self.get_view_paths(klass)
-
6
@all_view_paths[klass] || get_view_paths(klass.superclass)
-
end
-
-
1
def self.set_view_paths(klass, paths)
-
8
@all_view_paths[klass] = paths
-
end
-
-
1
def self.all_view_paths
-
@all_view_paths.values.uniq
-
end
-
# :startdoc:
-
-
# The prefixes used in render "foo" shortcuts.
-
1
def _prefixes # :nodoc:
-
self.class._prefixes
-
end
-
-
# <tt>LookupContext</tt> is the object responsible for holding all
-
# information required for looking up templates, i.e. view paths and
-
# details. Check <tt>ActionView::LookupContext</tt> for more information.
-
1
def lookup_context
-
@_lookup_context ||=
-
ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
-
end
-
-
1
def details_for_lookup
-
{}
-
end
-
-
# Append a path to the list of view paths for the current <tt>LookupContext</tt>.
-
#
-
# ==== Parameters
-
# * <tt>path</tt> - If a String is provided, it gets converted into
-
# the default view path. You may also provide a custom view path
-
# (see ActionView::PathSet for more information)
-
1
def append_view_path(path)
-
lookup_context.view_paths.push(*path)
-
end
-
-
# Prepend a path to the list of view paths for the current <tt>LookupContext</tt>.
-
#
-
# ==== Parameters
-
# * <tt>path</tt> - If a String is provided, it gets converted into
-
# the default view path. You may also provide a custom view path
-
# (see ActionView::PathSet for more information)
-
1
def prepend_view_path(path)
-
lookup_context.view_paths.unshift(*path)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/class/subclasses"
-
1
require "active_support/testing/assertions"
-
-
1
module ActiveJob
-
# Provides helper methods for testing Active Job
-
1
module TestHelper
-
1
delegate :enqueued_jobs, :enqueued_jobs=,
-
:performed_jobs, :performed_jobs=,
-
to: :queue_adapter
-
-
1
include ActiveSupport::Testing::Assertions
-
-
1
module TestQueueAdapter
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
class_attribute :_test_adapter, instance_accessor: false, instance_predicate: false
-
end
-
-
1
module ClassMethods
-
1
def queue_adapter
-
self._test_adapter.nil? ? super : self._test_adapter
-
end
-
-
1
def disable_test_adapter
-
self._test_adapter = nil
-
end
-
-
1
def enable_test_adapter(test_adapter)
-
self._test_adapter = test_adapter
-
end
-
end
-
end
-
-
1
ActiveJob::Base.include(TestQueueAdapter)
-
-
1
def before_setup # :nodoc:
-
test_adapter = queue_adapter_for_test
-
-
queue_adapter_changed_jobs.each do |klass|
-
klass.enable_test_adapter(test_adapter)
-
end
-
-
clear_enqueued_jobs
-
clear_performed_jobs
-
super
-
end
-
-
1
def after_teardown # :nodoc:
-
super
-
-
queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter }
-
end
-
-
# Specifies the queue adapter to use with all Active Job test helpers.
-
#
-
# Returns an instance of the queue adapter and defaults to
-
# <tt>ActiveJob::QueueAdapters::TestAdapter</tt>.
-
#
-
# Note: The adapter provided by this method must provide some additional
-
# methods from those expected of a standard <tt>ActiveJob::QueueAdapter</tt>
-
# in order to be used with the active job test helpers. Refer to
-
# <tt>ActiveJob::QueueAdapters::TestAdapter</tt>.
-
1
def queue_adapter_for_test
-
ActiveJob::QueueAdapters::TestAdapter.new
-
end
-
-
# Asserts that the number of enqueued jobs matches the given number.
-
#
-
# def test_jobs
-
# assert_enqueued_jobs 0
-
# HelloJob.perform_later('david')
-
# assert_enqueued_jobs 1
-
# HelloJob.perform_later('abdelkader')
-
# assert_enqueued_jobs 2
-
# end
-
#
-
# If a block is passed, asserts that the block will cause the specified number of
-
# jobs to be enqueued.
-
#
-
# def test_jobs_again
-
# assert_enqueued_jobs 1 do
-
# HelloJob.perform_later('cristian')
-
# end
-
#
-
# assert_enqueued_jobs 2 do
-
# HelloJob.perform_later('aaron')
-
# HelloJob.perform_later('rafael')
-
# end
-
# end
-
#
-
# Asserts the number of times a specific job was enqueued by passing +:only+ option.
-
#
-
# def test_logging_job
-
# assert_enqueued_jobs 1, only: LoggingJob do
-
# LoggingJob.perform_later
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# Asserts the number of times a job except specific class was enqueued by passing +:except+ option.
-
#
-
# def test_logging_job
-
# assert_enqueued_jobs 1, except: HelloJob do
-
# LoggingJob.perform_later
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
-
# a hash containing the job's class and it's argument are passed as argument.
-
#
-
# Asserts the number of times a job is enqueued to a specific queue by passing +:queue+ option.
-
#
-
# def test_logging_job
-
# assert_enqueued_jobs 2, queue: 'default' do
-
# LoggingJob.perform_later
-
# HelloJob.perform_later('elfassy')
-
# end
-
# end
-
1
def assert_enqueued_jobs(number, only: nil, except: nil, queue: nil, &block)
-
if block_given?
-
original_jobs = enqueued_jobs_with(only: only, except: except, queue: queue)
-
-
assert_nothing_raised(&block)
-
-
new_jobs = enqueued_jobs_with(only: only, except: except, queue: queue)
-
-
actual_count = (new_jobs - original_jobs).count
-
else
-
actual_count = enqueued_jobs_with(only: only, except: except, queue: queue).count
-
end
-
-
assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
-
end
-
-
# Asserts that no jobs have been enqueued.
-
#
-
# def test_jobs
-
# assert_no_enqueued_jobs
-
# HelloJob.perform_later('jeremy')
-
# assert_enqueued_jobs 1
-
# end
-
#
-
# If a block is passed, asserts that the block will not cause any job to be enqueued.
-
#
-
# def test_jobs_again
-
# assert_no_enqueued_jobs do
-
# # No job should be enqueued from this block
-
# end
-
# end
-
#
-
# Asserts that no jobs of a specific kind are enqueued by passing +:only+ option.
-
#
-
# def test_no_logging
-
# assert_no_enqueued_jobs only: LoggingJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# Asserts that no jobs except specific class are enqueued by passing +:except+ option.
-
#
-
# def test_no_logging
-
# assert_no_enqueued_jobs except: HelloJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
-
# a hash containing the job's class and it's argument are passed as argument.
-
#
-
# Asserts that no jobs are enqueued to a specific queue by passing +:queue+ option
-
#
-
# def test_no_logging
-
# assert_no_enqueued_jobs queue: 'default' do
-
# LoggingJob.set(queue: :some_queue).perform_later
-
# end
-
# end
-
#
-
# Note: This assertion is simply a shortcut for:
-
#
-
# assert_enqueued_jobs 0, &block
-
1
def assert_no_enqueued_jobs(only: nil, except: nil, queue: nil, &block)
-
assert_enqueued_jobs 0, only: only, except: except, queue: queue, &block
-
end
-
-
# Asserts that the number of performed jobs matches the given number.
-
# If no block is passed, <tt>perform_enqueued_jobs</tt>
-
# must be called around or after the job call.
-
#
-
# def test_jobs
-
# assert_performed_jobs 0
-
#
-
# perform_enqueued_jobs do
-
# HelloJob.perform_later('xavier')
-
# end
-
# assert_performed_jobs 1
-
#
-
# HelloJob.perform_later('yves')
-
#
-
# perform_enqueued_jobs
-
#
-
# assert_performed_jobs 2
-
# end
-
#
-
# If a block is passed, asserts that the block will cause the specified number of
-
# jobs to be performed.
-
#
-
# def test_jobs_again
-
# assert_performed_jobs 1 do
-
# HelloJob.perform_later('robin')
-
# end
-
#
-
# assert_performed_jobs 2 do
-
# HelloJob.perform_later('carlos')
-
# HelloJob.perform_later('sean')
-
# end
-
# end
-
#
-
# This method also supports filtering. If the +:only+ option is specified,
-
# then only the listed job(s) will be performed.
-
#
-
# def test_hello_job
-
# assert_performed_jobs 1, only: HelloJob do
-
# HelloJob.perform_later('jeremy')
-
# LoggingJob.perform_later
-
# end
-
# end
-
#
-
# Also if the +:except+ option is specified,
-
# then the job(s) except specific class will be performed.
-
#
-
# def test_hello_job
-
# assert_performed_jobs 1, except: LoggingJob do
-
# HelloJob.perform_later('jeremy')
-
# LoggingJob.perform_later
-
# end
-
# end
-
#
-
# An array may also be specified, to support testing multiple jobs.
-
#
-
# def test_hello_and_logging_jobs
-
# assert_nothing_raised do
-
# assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
-
# HelloJob.perform_later('jeremy')
-
# LoggingJob.perform_later('stewie')
-
# RescueJob.perform_later('david')
-
# end
-
# end
-
# end
-
#
-
# A proc may also be specified. When passed a Proc, the job's instance will be passed as argument.
-
#
-
# def test_hello_and_logging_jobs
-
# assert_nothing_raised do
-
# assert_performed_jobs(1, only: ->(job) { job.is_a?(HelloJob) }) do
-
# HelloJob.perform_later('jeremy')
-
# LoggingJob.perform_later('stewie')
-
# RescueJob.perform_later('david')
-
# end
-
# end
-
# end
-
#
-
# If the +:queue+ option is specified,
-
# then only the job(s) enqueued to a specific queue will be performed.
-
#
-
# def test_assert_performed_jobs_with_queue_option
-
# assert_performed_jobs 1, queue: :some_queue do
-
# HelloJob.set(queue: :some_queue).perform_later("jeremy")
-
# HelloJob.set(queue: :other_queue).perform_later("bogdan")
-
# end
-
# end
-
1
def assert_performed_jobs(number, only: nil, except: nil, queue: nil, &block)
-
if block_given?
-
original_count = performed_jobs.size
-
-
perform_enqueued_jobs(only: only, except: except, queue: queue, &block)
-
-
new_count = performed_jobs.size
-
-
performed_jobs_size = new_count - original_count
-
else
-
performed_jobs_size = performed_jobs_with(only: only, except: except, queue: queue).count
-
end
-
-
assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
-
end
-
-
# Asserts that no jobs have been performed.
-
#
-
# def test_jobs
-
# assert_no_performed_jobs
-
#
-
# perform_enqueued_jobs do
-
# HelloJob.perform_later('matthew')
-
# assert_performed_jobs 1
-
# end
-
# end
-
#
-
# If a block is passed, asserts that the block will not cause any job to be performed.
-
#
-
# def test_jobs_again
-
# assert_no_performed_jobs do
-
# # No job should be performed from this block
-
# end
-
# end
-
#
-
# The block form supports filtering. If the +:only+ option is specified,
-
# then only the listed job(s) will not be performed.
-
#
-
# def test_no_logging
-
# assert_no_performed_jobs only: LoggingJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# Also if the +:except+ option is specified,
-
# then the job(s) except specific class will not be performed.
-
#
-
# def test_no_logging
-
# assert_no_performed_jobs except: HelloJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
-
# an instance of the job will be passed as argument.
-
#
-
# If the +:queue+ option is specified,
-
# then only the job(s) enqueued to a specific queue will not be performed.
-
#
-
# def test_assert_no_performed_jobs_with_queue_option
-
# assert_no_performed_jobs queue: :some_queue do
-
# HelloJob.set(queue: :other_queue).perform_later("jeremy")
-
# end
-
# end
-
#
-
# Note: This assertion is simply a shortcut for:
-
#
-
# assert_performed_jobs 0, &block
-
1
def assert_no_performed_jobs(only: nil, except: nil, queue: nil, &block)
-
assert_performed_jobs 0, only: only, except: except, queue: queue, &block
-
end
-
-
# Asserts that the job has been enqueued with the given arguments.
-
#
-
# def test_assert_enqueued_with
-
# MyJob.perform_later(1,2,3)
-
# assert_enqueued_with(job: MyJob, args: [1,2,3])
-
#
-
# MyJob.set(wait_until: Date.tomorrow.noon, queue: "my_queue").perform_later
-
# assert_enqueued_with(at: Date.tomorrow.noon, queue: "my_queue")
-
# end
-
#
-
# The given arguments may also be specified as matcher procs that return a
-
# boolean value indicating whether a job's attribute meets certain criteria.
-
#
-
# For example, a proc can be used to match a range of times:
-
#
-
# def test_assert_enqueued_with
-
# at_matcher = ->(job_at) { (Date.yesterday..Date.tomorrow).cover?(job_at) }
-
#
-
# MyJob.set(wait_until: Date.today.noon).perform_later
-
#
-
# assert_enqueued_with(job: MyJob, at: at_matcher)
-
# end
-
#
-
# A proc can also be used to match a subset of a job's args:
-
#
-
# def test_assert_enqueued_with
-
# args_matcher = ->(job_args) { job_args[0].key?(:foo) }
-
#
-
# MyJob.perform_later(foo: "bar", other_arg: "No need to check in the test")
-
#
-
# assert_enqueued_with(job: MyJob, args: args_matcher)
-
# end
-
#
-
# If a block is passed, asserts that the block will cause the job to be
-
# enqueued with the given arguments.
-
#
-
# def test_assert_enqueued_with
-
# assert_enqueued_with(job: MyJob, args: [1,2,3]) do
-
# MyJob.perform_later(1,2,3)
-
# end
-
#
-
# assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) do
-
# MyJob.set(wait_until: Date.tomorrow.noon).perform_later
-
# end
-
# end
-
1
def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil, &block)
-
expected = { job: job, args: args, at: at, queue: queue }.compact
-
expected_args = prepare_args_for_assertion(expected)
-
potential_matches = []
-
-
if block_given?
-
original_enqueued_jobs = enqueued_jobs.dup
-
-
assert_nothing_raised(&block)
-
-
jobs = enqueued_jobs - original_enqueued_jobs
-
else
-
jobs = enqueued_jobs
-
end
-
-
matching_job = jobs.find do |enqueued_job|
-
deserialized_job = deserialize_args_for_assertion(enqueued_job)
-
potential_matches << deserialized_job
-
-
expected_args.all? do |key, value|
-
if value.respond_to?(:call)
-
value.call(deserialized_job[key])
-
else
-
value == deserialized_job[key]
-
end
-
end
-
end
-
-
message = +"No enqueued job found with #{expected}"
-
message << "\n\nPotential matches: #{potential_matches.join("\n")}" if potential_matches.present?
-
assert matching_job, message
-
instantiate_job(matching_job)
-
end
-
-
# Asserts that the job has been performed with the given arguments.
-
#
-
# def test_assert_performed_with
-
# MyJob.perform_later(1,2,3)
-
#
-
# perform_enqueued_jobs
-
#
-
# assert_performed_with(job: MyJob, args: [1,2,3])
-
#
-
# MyJob.set(wait_until: Date.tomorrow.noon, queue: "my_queue").perform_later
-
#
-
# perform_enqueued_jobs
-
#
-
# assert_performed_with(at: Date.tomorrow.noon, queue: "my_queue")
-
# end
-
#
-
# The given arguments may also be specified as matcher procs that return a
-
# boolean value indicating whether a job's attribute meets certain criteria.
-
#
-
# For example, a proc can be used to match a range of times:
-
#
-
# def test_assert_performed_with
-
# at_matcher = ->(job_at) { (Date.yesterday..Date.tomorrow).cover?(job_at) }
-
#
-
# MyJob.set(wait_until: Date.today.noon).perform_later
-
#
-
# perform_enqueued_jobs
-
#
-
# assert_performed_with(job: MyJob, at: at_matcher)
-
# end
-
#
-
# A proc can also be used to match a subset of a job's args:
-
#
-
# def test_assert_performed_with
-
# args_matcher = ->(job_args) { job_args[0].key?(:foo) }
-
#
-
# MyJob.perform_later(foo: "bar", other_arg: "No need to check in the test")
-
#
-
# perform_enqueued_jobs
-
#
-
# assert_performed_with(job: MyJob, args: args_matcher)
-
# end
-
#
-
# If a block is passed, that block performs all of the jobs that were
-
# enqueued throughout the duration of the block and asserts that
-
# the job has been performed with the given arguments in the block.
-
#
-
# def test_assert_performed_with
-
# assert_performed_with(job: MyJob, args: [1,2,3]) do
-
# MyJob.perform_later(1,2,3)
-
# end
-
#
-
# assert_performed_with(job: MyJob, at: Date.tomorrow.noon) do
-
# MyJob.set(wait_until: Date.tomorrow.noon).perform_later
-
# end
-
# end
-
1
def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block)
-
expected = { job: job, args: args, at: at, queue: queue }.compact
-
expected_args = prepare_args_for_assertion(expected)
-
potential_matches = []
-
-
if block_given?
-
original_performed_jobs_count = performed_jobs.count
-
-
perform_enqueued_jobs(&block)
-
-
jobs = performed_jobs.drop(original_performed_jobs_count)
-
else
-
jobs = performed_jobs
-
end
-
-
matching_job = jobs.find do |enqueued_job|
-
deserialized_job = deserialize_args_for_assertion(enqueued_job)
-
potential_matches << deserialized_job
-
-
expected_args.all? do |key, value|
-
if value.respond_to?(:call)
-
value.call(deserialized_job[key])
-
else
-
value == deserialized_job[key]
-
end
-
end
-
end
-
-
message = +"No performed job found with #{expected}"
-
message << "\n\nPotential matches: #{potential_matches.join("\n")}" if potential_matches.present?
-
assert matching_job, message
-
-
instantiate_job(matching_job)
-
end
-
-
# Performs all enqueued jobs. If a block is given, performs all of the jobs
-
# that were enqueued throughout the duration of the block. If a block is
-
# not given, performs all of the enqueued jobs up to this point in the test.
-
#
-
# def test_perform_enqueued_jobs
-
# perform_enqueued_jobs do
-
# MyJob.perform_later(1, 2, 3)
-
# end
-
# assert_performed_jobs 1
-
# end
-
#
-
# def test_perform_enqueued_jobs_without_block
-
# MyJob.perform_later(1, 2, 3)
-
#
-
# perform_enqueued_jobs
-
#
-
# assert_performed_jobs 1
-
# end
-
#
-
# This method also supports filtering. If the +:only+ option is specified,
-
# then only the listed job(s) will be performed.
-
#
-
# def test_perform_enqueued_jobs_with_only
-
# perform_enqueued_jobs(only: MyJob) do
-
# MyJob.perform_later(1, 2, 3) # will be performed
-
# HelloJob.perform_later(1, 2, 3) # will not be performed
-
# end
-
# assert_performed_jobs 1
-
# end
-
#
-
# Also if the +:except+ option is specified,
-
# then the job(s) except specific class will be performed.
-
#
-
# def test_perform_enqueued_jobs_with_except
-
# perform_enqueued_jobs(except: HelloJob) do
-
# MyJob.perform_later(1, 2, 3) # will be performed
-
# HelloJob.perform_later(1, 2, 3) # will not be performed
-
# end
-
# assert_performed_jobs 1
-
# end
-
#
-
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
-
# an instance of the job will be passed as argument.
-
#
-
# If the +:queue+ option is specified,
-
# then only the job(s) enqueued to a specific queue will be performed.
-
#
-
# def test_perform_enqueued_jobs_with_queue
-
# perform_enqueued_jobs queue: :some_queue do
-
# MyJob.set(queue: :some_queue).perform_later(1, 2, 3) # will be performed
-
# HelloJob.set(queue: :other_queue).perform_later(1, 2, 3) # will not be performed
-
# end
-
# assert_performed_jobs 1
-
# end
-
#
-
# If the +:at+ option is specified, then only run jobs enqueued to run
-
# immediately or before the given time
-
1
def perform_enqueued_jobs(only: nil, except: nil, queue: nil, at: nil, &block)
-
return flush_enqueued_jobs(only: only, except: except, queue: queue, at: at) unless block_given?
-
-
validate_option(only: only, except: except)
-
-
old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
-
old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
-
old_filter = queue_adapter.filter
-
old_reject = queue_adapter.reject
-
old_queue = queue_adapter.queue
-
old_at = queue_adapter.at
-
-
begin
-
queue_adapter.perform_enqueued_jobs = true
-
queue_adapter.perform_enqueued_at_jobs = true
-
queue_adapter.filter = only
-
queue_adapter.reject = except
-
queue_adapter.queue = queue
-
queue_adapter.at = at
-
-
assert_nothing_raised(&block)
-
ensure
-
queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs
-
queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs
-
queue_adapter.filter = old_filter
-
queue_adapter.reject = old_reject
-
queue_adapter.queue = old_queue
-
queue_adapter.at = old_at
-
end
-
end
-
-
# Accesses the queue_adapter set by ActiveJob::Base.
-
#
-
# def test_assert_job_has_custom_queue_adapter_set
-
# assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter
-
# end
-
1
def queue_adapter
-
ActiveJob::Base.queue_adapter
-
end
-
-
1
private
-
1
def clear_enqueued_jobs
-
enqueued_jobs.clear
-
end
-
-
1
def clear_performed_jobs
-
performed_jobs.clear
-
end
-
-
1
def jobs_with(jobs, only: nil, except: nil, queue: nil, at: nil)
-
validate_option(only: only, except: except)
-
-
jobs.dup.select do |job|
-
job_class = job.fetch(:job)
-
-
if only
-
next false unless filter_as_proc(only).call(job)
-
elsif except
-
next false if filter_as_proc(except).call(job)
-
end
-
-
if queue
-
next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
-
end
-
-
if at && job[:at]
-
next false if job[:at] > at.to_f
-
end
-
-
yield job if block_given?
-
-
true
-
end
-
end
-
-
1
def filter_as_proc(filter)
-
return filter if filter.is_a?(Proc)
-
-
->(job) { Array(filter).include?(job.fetch(:job)) }
-
end
-
-
1
def enqueued_jobs_with(only: nil, except: nil, queue: nil, at: nil, &block)
-
jobs_with(enqueued_jobs, only: only, except: except, queue: queue, at: at, &block)
-
end
-
-
1
def performed_jobs_with(only: nil, except: nil, queue: nil, &block)
-
jobs_with(performed_jobs, only: only, except: except, queue: queue, &block)
-
end
-
-
1
def flush_enqueued_jobs(only: nil, except: nil, queue: nil, at: nil)
-
enqueued_jobs_with(only: only, except: except, queue: queue, at: at) do |payload|
-
queue_adapter.enqueued_jobs.delete(payload)
-
queue_adapter.performed_jobs << payload
-
instantiate_job(payload).perform_now
-
end.count
-
end
-
-
1
def prepare_args_for_assertion(args)
-
args.dup.tap do |arguments|
-
if arguments[:at].acts_like?(:time)
-
at_range = arguments[:at] - 1..arguments[:at] + 1
-
arguments[:at] = ->(at) { at_range.cover?(at) }
-
end
-
end
-
end
-
-
1
def deserialize_args_for_assertion(job)
-
job.dup.tap do |new_job|
-
new_job[:at] = Time.at(new_job[:at]) if new_job[:at]
-
new_job[:args] = ActiveJob::Arguments.deserialize(new_job[:args]) if new_job[:args]
-
end
-
end
-
-
1
def instantiate_job(payload)
-
job = payload[:job].deserialize(payload)
-
job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
-
job.send(:deserialize_arguments_if_needed)
-
job
-
end
-
-
1
def queue_adapter_changed_jobs
-
(ActiveJob::Base.descendants << ActiveJob::Base).select do |klass|
-
# only override explicitly set adapters, a quirk of `class_attribute`
-
klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
-
end
-
end
-
-
1
def validate_option(only: nil, except: nil)
-
raise ArgumentError, "Cannot specify both `:only` and `:except` options." if only && except
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/string/conversions"
-
-
1
module ActiveRecord
-
1
module Associations
-
# Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
-
1
class AliasTracker # :nodoc:
-
1
def self.create(connection, initial_table, joins, aliases = nil)
-
6
if joins.empty?
-
6
aliases ||= Hash.new(0)
-
elsif aliases
-
default_proc = aliases.default_proc || proc { 0 }
-
aliases.default_proc = proc { |h, k|
-
h[k] = initial_count_for(connection, k, joins) + default_proc.call(h, k)
-
}
-
else
-
aliases = Hash.new { |h, k|
-
h[k] = initial_count_for(connection, k, joins)
-
}
-
end
-
6
aliases[initial_table] = 1
-
6
new(connection, aliases)
-
end
-
-
1
def self.initial_count_for(connection, name, table_joins)
-
quoted_name = nil
-
-
counts = table_joins.map do |join|
-
if join.is_a?(Arel::Nodes::StringJoin)
-
# quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
-
quoted_name ||= connection.quote_table_name(name)
-
-
# Table names + table aliases
-
join.left.scan(
-
/JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
-
).size
-
elsif join.is_a?(Arel::Nodes::Join)
-
join.left.name == name ? 1 : 0
-
else
-
raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
-
end
-
end
-
-
counts.sum
-
end
-
-
# table_joins is an array of arel joins which might conflict with the aliases we assign here
-
1
def initialize(connection, aliases)
-
6
@aliases = aliases
-
6
@connection = connection
-
end
-
-
1
def aliased_table_for(arel_table, table_name = nil)
-
table_name ||= arel_table.name
-
-
if aliases[table_name] == 0
-
# If it's zero, we can have our table_name
-
aliases[table_name] = 1
-
arel_table = arel_table.alias(table_name) if arel_table.name != table_name
-
else
-
# Otherwise, we need to use an alias
-
aliased_name = @connection.table_alias_for(yield)
-
-
# Update the count
-
count = aliases[aliased_name] += 1
-
-
aliased_name = "#{truncate(aliased_name)}_#{count}" if count > 1
-
-
arel_table = arel_table.alias(aliased_name)
-
end
-
-
arel_table
-
end
-
-
1
attr_reader :aliases
-
-
1
private
-
1
def truncate(name)
-
name.slice(0, @connection.table_alias_length - 2)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Associations
-
# = Active Record Associations
-
#
-
# This is the root class of all associations ('+ Foo' signifies an included module Foo):
-
#
-
# Association
-
# SingularAssociation
-
# HasOneAssociation + ForeignAssociation
-
# HasOneThroughAssociation + ThroughAssociation
-
# BelongsToAssociation
-
# BelongsToPolymorphicAssociation
-
# CollectionAssociation
-
# HasManyAssociation + ForeignAssociation
-
# HasManyThroughAssociation + ThroughAssociation
-
#
-
# Associations in Active Record are middlemen between the object that
-
# holds the association, known as the <tt>owner</tt>, and the associated
-
# result set, known as the <tt>target</tt>. Association metadata is available in
-
# <tt>reflection</tt>, which is an instance of <tt>ActiveRecord::Reflection::AssociationReflection</tt>.
-
#
-
# For example, given
-
#
-
# class Blog < ActiveRecord::Base
-
# has_many :posts
-
# end
-
#
-
# blog = Blog.first
-
#
-
# The association of <tt>blog.posts</tt> has the object +blog+ as its
-
# <tt>owner</tt>, the collection of its posts as <tt>target</tt>, and
-
# the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro.
-
1
class Association #:nodoc:
-
1
attr_reader :owner, :target, :reflection
-
-
1
delegate :options, to: :reflection
-
-
1
def initialize(owner, reflection)
-
8
reflection.check_validity!
-
-
8
@owner, @reflection = owner, reflection
-
-
8
reset
-
8
reset_scope
-
end
-
-
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
-
1
def reset
-
11
@loaded = false
-
11
@target = nil
-
11
@stale_state = nil
-
11
@inversed = false
-
end
-
-
1
def reset_negative_cache # :nodoc:
-
reset if loaded? && target.nil?
-
end
-
-
# Reloads the \target and returns +self+ on success.
-
# The QueryCache is cleared if +force+ is true.
-
1
def reload(force = false)
-
3
klass.connection.clear_query_cache if force && klass
-
3
reset
-
3
reset_scope
-
3
load_target
-
3
self unless target.nil?
-
end
-
-
# Has the \target been already \loaded?
-
1
def loaded?
-
45
@loaded
-
end
-
-
# Asserts the \target has been loaded setting the \loaded flag to +true+.
-
1
def loaded!
-
9
@loaded = true
-
9
@stale_state = stale_state
-
9
@inversed = false
-
end
-
-
# The target is stale if the target no longer points to the record(s) that the
-
# relevant foreign_key(s) refers to. If stale, the association accessor method
-
# on the owner will reload the target. It's up to subclasses to implement the
-
# stale_state method if relevant.
-
#
-
# Note that if the target has not been loaded, it is not considered stale.
-
1
def stale_target?
-
18
!@inversed && loaded? && @stale_state != stale_state
-
end
-
-
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
-
1
def target=(target)
-
6
@target = target
-
6
loaded!
-
end
-
-
1
def scope
-
9
if (scope = klass.current_scope) && scope.try(:proxy_association) == self
-
scope.spawn
-
else
-
9
target_scope.merge!(association_scope)
-
end
-
end
-
-
1
def reset_scope
-
11
@association_scope = nil
-
end
-
-
# Set the inverse association, if possible
-
1
def set_inverse_instance(record)
-
9
if inverse = inverse_association_for(record)
-
6
inverse.inversed_from(owner)
-
end
-
9
record
-
end
-
-
1
def set_inverse_instance_from_queries(record)
-
if inverse = inverse_association_for(record)
-
inverse.inversed_from_queries(owner)
-
end
-
record
-
end
-
-
# Remove the inverse association, if possible
-
1
def remove_inverse_instance(record)
-
if inverse = inverse_association_for(record)
-
inverse.inversed_from(nil)
-
end
-
end
-
-
1
def inversed_from(record)
-
6
self.target = record
-
6
@inversed = !!record
-
end
-
-
1
def inversed_from_queries(record)
-
if inversable?(record)
-
self.target = record
-
@inversed = true
-
else
-
@inversed = false
-
end
-
end
-
-
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
-
# polymorphic_type field on the owner.
-
1
def klass
-
75
reflection.klass
-
end
-
-
1
def extensions
-
2
extensions = klass.default_extensions | reflection.extensions
-
-
2
if reflection.scope
-
extensions |= reflection.scope_for(klass.unscoped, owner).extensions
-
end
-
-
2
extensions
-
end
-
-
# Loads the \target if needed and returns it.
-
#
-
# This method is abstract in the sense that it relies on +find_target+,
-
# which is expected to be provided by descendants.
-
#
-
# If the \target is already \loaded it is just returned. Thus, you can call
-
# +load_target+ unconditionally to get the \target.
-
#
-
# ActiveRecord::RecordNotFound is rescued within the method, and it is
-
# not reraised. The proxy is \reset and +nil+ is the return value.
-
1
def load_target
-
9
@target = find_target if (@stale_state && stale_target?) || find_target?
-
-
9
loaded! unless loaded?
-
9
target
-
rescue ActiveRecord::RecordNotFound
-
reset
-
end
-
-
# We can't dump @reflection and @through_reflection since it contains the scope proc
-
1
def marshal_dump
-
ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
-
[@reflection.name, ivars]
-
end
-
-
1
def marshal_load(data)
-
reflection_name, ivars = data
-
ivars.each { |name, val| instance_variable_set(name, val) }
-
@reflection = @owner.class._reflect_on_association(reflection_name)
-
end
-
-
1
def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc:
-
3
except_from_scope_attributes ||= {}
-
3
skip_assign = [reflection.foreign_key, reflection.type].compact
-
3
assigned_keys = record.changed_attribute_names_to_save
-
3
assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
-
3
attributes = scope_for_create.except!(*(assigned_keys - skip_assign))
-
3
record.send(:_assign_attributes, attributes) if attributes.any?
-
3
set_inverse_instance(record)
-
end
-
-
1
def create(attributes = nil, &block)
-
3
_create_record(attributes, &block)
-
end
-
-
1
def create!(attributes = nil, &block)
-
_create_record(attributes, true, &block)
-
end
-
-
1
private
-
1
def find_target
-
3
if (owner.strict_loading? || reflection.strict_loading?) && owner.validation_context.nil?
-
Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
-
end
-
-
3
scope = self.scope
-
3
return scope.to_a if skip_statement_cache?(scope)
-
-
3
sc = reflection.association_scope_cache(klass, owner) do |params|
-
2
as = AssociationScope.create { params.bind }
-
1
target_scope.merge!(as.scope(self))
-
end
-
-
3
binds = AssociationScope.get_bind_values(owner, reflection.chain)
-
6
sc.execute(binds, klass.connection) { |record| set_inverse_instance(record) }
-
end
-
-
# The scope for this association.
-
#
-
# Note that the association_scope is merged into the target_scope only when the
-
# scope method is called. This is because at that point the call may be surrounded
-
# by scope.scoping { ... } or unscoped { ... } etc, which affects the scope which
-
# actually gets built.
-
1
def association_scope
-
12
if klass
-
12
@association_scope ||= AssociationScope.scope(self)
-
end
-
end
-
-
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
-
# through association's scope)
-
1
def target_scope
-
10
AssociationRelation.create(klass, self).merge!(klass.scope_for_association)
-
end
-
-
1
def scope_for_create
-
3
scope.scope_for_create
-
end
-
-
1
def find_target?
-
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
-
end
-
-
# Returns true if there is a foreign key present on the owner which
-
# references the target. This is used to determine whether we can load
-
# the target if the owner is currently a new record (and therefore
-
# without a key). If the owner is a new record then foreign_key must
-
# be present in order to load target.
-
#
-
# Currently implemented by belongs_to (vanilla and polymorphic) and
-
# has_one/has_many :through associations which go through a belongs_to.
-
1
def foreign_key_present?
-
false
-
end
-
-
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
-
# the kind of the class of the associated objects. Meant to be used as
-
# a sanity check when you are about to assign an associated record.
-
1
def raise_on_type_mismatch!(record)
-
unless record.is_a?(reflection.klass)
-
fresh_class = reflection.class_name.safe_constantize
-
unless fresh_class && record.is_a?(fresh_class)
-
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
-
"got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
-
raise ActiveRecord::AssociationTypeMismatch, message
-
end
-
end
-
end
-
-
1
def inverse_association_for(record)
-
9
if invertible_for?(record)
-
6
record.association(inverse_reflection_for(record).name)
-
end
-
end
-
-
# Can be redefined by subclasses, notably polymorphic belongs_to
-
# The record parameter is necessary to support polymorphic inverses as we must check for
-
# the association in the specific class of the record.
-
1
def inverse_reflection_for(record)
-
15
reflection.inverse_of
-
end
-
-
# Returns true if inverse association on the given record needs to be set.
-
# This method is redefined by subclasses.
-
1
def invertible_for?(record)
-
6
foreign_key_for?(record) && inverse_reflection_for(record)
-
end
-
-
# Returns true if record contains the foreign_key
-
1
def foreign_key_for?(record)
-
6
record._has_attribute?(reflection.foreign_key)
-
end
-
-
# This should be implemented to return the values of the relevant key(s) on the owner,
-
# so that when stale_state is different from the value stored on the last find_target,
-
# the target is stale.
-
#
-
# This is only relevant to certain associations, which is why it returns +nil+ by default.
-
1
def stale_state
-
end
-
-
1
def build_record(attributes)
-
3
reflection.build_association(attributes) do |record|
-
3
initialize_attributes(record, attributes)
-
3
yield(record) if block_given?
-
end
-
end
-
-
# Returns true if statement cache should be skipped on the association reader.
-
1
def skip_statement_cache?(scope)
-
3
reflection.has_scope? ||
-
scope.eager_loading? ||
-
klass.scope_attributes? ||
-
reflection.source_reflection.active_record.default_scopes.any?
-
end
-
-
1
def enqueue_destroy_association(options)
-
job_class = owner.class.destroy_association_async_job
-
-
if job_class
-
owner._after_commit_jobs.push([job_class, options])
-
end
-
end
-
-
1
def inversable?(record)
-
record &&
-
((!record.persisted? || !owner.persisted?) || matches_foreign_key?(record))
-
end
-
-
1
def matches_foreign_key?(record)
-
if foreign_key_for?(record)
-
record.read_attribute(reflection.foreign_key) == owner.id ||
-
(foreign_key_for?(owner) && owner.read_attribute(reflection.foreign_key) == record.id)
-
else
-
owner.read_attribute(reflection.foreign_key) == record.id
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Associations
-
1
class AssociationScope #:nodoc:
-
1
def self.scope(association)
-
5
INSTANCE.scope(association)
-
end
-
-
1
def self.create(&block)
-
7
block ||= lambda { |val| val }
-
2
new(block)
-
end
-
-
1
def initialize(value_transformation)
-
2
@value_transformation = value_transformation
-
end
-
-
1
INSTANCE = create
-
-
1
def scope(association)
-
6
klass = association.klass
-
6
reflection = association.reflection
-
6
scope = klass.unscoped
-
6
owner = association.owner
-
6
chain = get_chain(reflection, association, scope.alias_tracker)
-
-
6
scope.extending! reflection.extensions
-
6
scope = add_constraints(scope, owner, chain)
-
6
scope.limit!(1) unless reflection.collection?
-
6
scope
-
end
-
-
1
def self.get_bind_values(owner, chain)
-
3
binds = []
-
3
last_reflection = chain.last
-
-
3
binds << last_reflection.join_id_for(owner)
-
3
if last_reflection.type
-
binds << owner.class.polymorphic_name
-
end
-
-
3
chain.each_cons(2).each do |reflection, next_reflection|
-
if reflection.type
-
binds << next_reflection.klass.polymorphic_name
-
end
-
end
-
3
binds
-
end
-
-
1
private
-
1
attr_reader :value_transformation
-
-
1
def join(table, constraint)
-
Arel::Nodes::LeadingJoin.new(table, Arel::Nodes::On.new(constraint))
-
end
-
-
1
def last_chain_scope(scope, reflection, owner)
-
6
primary_key = reflection.join_primary_key
-
6
foreign_key = reflection.join_foreign_key
-
-
6
table = reflection.aliased_table
-
6
value = transform_value(owner[foreign_key])
-
6
scope = apply_scope(scope, table, primary_key, value)
-
-
6
if reflection.type
-
polymorphic_type = transform_value(owner.class.polymorphic_name)
-
scope = apply_scope(scope, table, reflection.type, polymorphic_type)
-
end
-
-
6
scope
-
end
-
-
1
def transform_value(value)
-
6
value_transformation.call(value)
-
end
-
-
1
def next_chain_scope(scope, reflection, next_reflection)
-
primary_key = reflection.join_primary_key
-
foreign_key = reflection.join_foreign_key
-
-
table = reflection.aliased_table
-
foreign_table = next_reflection.aliased_table
-
constraint = table[primary_key].eq(foreign_table[foreign_key])
-
-
if reflection.type
-
value = transform_value(next_reflection.klass.polymorphic_name)
-
scope = apply_scope(scope, table, reflection.type, value)
-
end
-
-
scope.joins!(join(foreign_table, constraint))
-
end
-
-
1
class ReflectionProxy < SimpleDelegator # :nodoc:
-
1
attr_reader :aliased_table
-
-
1
def initialize(reflection, aliased_table)
-
super(reflection)
-
@aliased_table = aliased_table
-
end
-
-
1
def all_includes; nil; end
-
end
-
-
1
def get_chain(reflection, association, tracker)
-
6
name = reflection.name
-
6
chain = [Reflection::RuntimeReflection.new(reflection, association)]
-
6
reflection.chain.drop(1).each do |refl|
-
aliased_table = tracker.aliased_table_for(refl.klass.arel_table) do
-
refl.alias_candidate(name)
-
end
-
chain << ReflectionProxy.new(refl, aliased_table)
-
end
-
6
chain
-
end
-
-
1
def add_constraints(scope, owner, chain)
-
6
scope = last_chain_scope(scope, chain.last, owner)
-
-
6
chain.each_cons(2) do |reflection, next_reflection|
-
scope = next_chain_scope(scope, reflection, next_reflection)
-
end
-
-
6
chain_head = chain.first
-
6
chain.reverse_each do |reflection|
-
# Exclude the scope of the association itself, because that
-
# was already merged in the #scope method.
-
6
reflection.constraints.each do |scope_chain_item|
-
item = eval_scope(reflection, scope_chain_item, owner)
-
-
if scope_chain_item == chain_head.scope
-
scope.merge! item.except(:where, :includes, :unscope, :order)
-
elsif !item.references_values.empty?
-
scope.merge! item.only(:joins, :left_outer_joins)
-
-
associations = item.eager_load_values | item.includes_values
-
-
unless associations.empty?
-
scope.joins! item.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
-
end
-
end
-
-
reflection.all_includes do
-
scope.includes_values |= item.includes_values
-
end
-
-
scope.unscope!(*item.unscope_values)
-
scope.where_clause += item.where_clause
-
scope.order_values = item.order_values | scope.order_values
-
end
-
end
-
-
6
scope
-
end
-
-
1
def apply_scope(scope, table, key, value)
-
6
if scope.table == table
-
6
scope.where!(key => value)
-
else
-
scope.where!(table.name => { key => value })
-
end
-
end
-
-
1
def eval_scope(reflection, scope, owner)
-
relation = reflection.build_scope(reflection.aliased_table)
-
relation.instance_exec(owner, &scope) || relation
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Associations
-
# = Active Record Belongs To Association
-
1
class BelongsToAssociation < SingularAssociation #:nodoc:
-
1
def handle_dependency
-
return unless load_target
-
-
case options[:dependent]
-
when :destroy
-
target.destroy
-
raise ActiveRecord::Rollback unless target.destroyed?
-
when :destroy_async
-
id = owner.public_send(reflection.foreign_key.to_sym)
-
primary_key_column = reflection.active_record_primary_key.to_sym
-
-
enqueue_destroy_association(
-
owner_model_name: owner.class.to_s,
-
owner_id: owner.id,
-
association_class: reflection.klass.to_s,
-
association_ids: [id],
-
association_primary_key_column: primary_key_column,
-
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
-
)
-
else
-
target.public_send(options[:dependent])
-
end
-
end
-
-
1
def inversed_from(record)
-
6
replace_keys(record)
-
6
super
-
end
-
-
1
def default(&block)
-
writer(owner.instance_exec(&block)) if reader.nil?
-
end
-
-
1
def reset
-
9
super
-
9
@updated = false
-
end
-
-
1
def updated?
-
6
@updated
-
end
-
-
1
def decrement_counters
-
update_counters(-1)
-
end
-
-
1
def increment_counters
-
update_counters(1)
-
end
-
-
1
def decrement_counters_before_last_save
-
if reflection.polymorphic?
-
model_was = owner.attribute_before_last_save(reflection.foreign_type)&.constantize
-
else
-
model_was = klass
-
end
-
-
foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key)
-
-
if foreign_key_was && model_was < ActiveRecord::Base
-
update_counters_via_scope(model_was, foreign_key_was, -1)
-
end
-
end
-
-
1
def target_changed?
-
owner.saved_change_to_attribute?(reflection.foreign_key)
-
end
-
-
1
private
-
1
def replace(record)
-
if record
-
raise_on_type_mismatch!(record)
-
set_inverse_instance(record)
-
@updated = true
-
end
-
-
replace_keys(record, force: true)
-
-
self.target = record
-
end
-
-
1
def update_counters(by)
-
if require_counter_update? && foreign_key_present?
-
if target && !stale_target?
-
target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
-
else
-
update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by)
-
end
-
end
-
end
-
-
1
def update_counters_via_scope(klass, foreign_key, by)
-
scope = klass.unscoped.where!(primary_key(klass) => foreign_key)
-
scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
-
end
-
-
1
def find_target?
-
9
!loaded? && foreign_key_present? && klass
-
end
-
-
1
def require_counter_update?
-
reflection.counter_cache_column && owner.persisted?
-
end
-
-
1
def replace_keys(record, force: false)
-
6
target_key = record ? record._read_attribute(primary_key(record.class)) : nil
-
-
6
if force || owner[reflection.foreign_key] != target_key
-
owner[reflection.foreign_key] = target_key
-
end
-
end
-
-
1
def primary_key(klass)
-
6
reflection.association_primary_key(klass)
-
end
-
-
1
def foreign_key_present?
-
3
owner._read_attribute(reflection.foreign_key)
-
end
-
-
1
def invertible_for?(record)
-
3
inverse = inverse_reflection_for(record)
-
3
inverse && (inverse.has_one? || ActiveRecord::Base.has_many_inversing)
-
end
-
-
1
def stale_state
-
15
result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
-
15
result && result.to_s
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Associations
-
# = Active Record Association Collection
-
#
-
# CollectionAssociation is an abstract class that provides common stuff to
-
# ease the implementation of association proxies that represent
-
# collections. See the class hierarchy in Association.
-
#
-
# CollectionAssociation:
-
# HasManyAssociation => has_many
-
# HasManyThroughAssociation + ThroughAssociation => has_many :through
-
#
-
# The CollectionAssociation class provides common methods to the collections
-
# defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
-
# the +:through association+ option.
-
#
-
# You need to be careful with assumptions regarding the target: The proxy
-
# does not fetch records from the database until it needs them, but new
-
# ones created with +build+ are added to the target. So, the target may be
-
# non-empty and still lack children waiting to be read from the database.
-
# If you look directly to the database you cannot assume that's the entire
-
# collection because new records may have been added to the target, etc.
-
#
-
# If you need to work on all current children, new and existing records,
-
# +load_target+ and the +loaded+ flag are your friends.
-
1
class CollectionAssociation < Association #:nodoc:
-
# Implements the reader method, e.g. foo.items for Foo.has_many :items
-
1
def reader
-
3
if stale_target?
-
reload
-
end
-
-
3
@proxy ||= CollectionProxy.create(klass, self)
-
3
@proxy.reset_scope
-
end
-
-
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
-
1
def writer(records)
-
replace(records)
-
end
-
-
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
-
1
def ids_reader
-
if loaded?
-
target.pluck(reflection.association_primary_key)
-
elsif !target.empty?
-
load_target.pluck(reflection.association_primary_key)
-
else
-
@association_ids ||= scope.pluck(reflection.association_primary_key)
-
end
-
end
-
-
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
-
1
def ids_writer(ids)
-
primary_key = reflection.association_primary_key
-
pk_type = klass.type_for_attribute(primary_key)
-
ids = Array(ids).compact_blank
-
ids.map! { |i| pk_type.cast(i) }
-
-
records = klass.where(primary_key => ids).index_by do |r|
-
r.public_send(primary_key)
-
end.values_at(*ids).compact
-
-
if records.size != ids.size
-
found_ids = records.map { |record| record.public_send(primary_key) }
-
not_found_ids = ids - found_ids
-
klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, primary_key, not_found_ids)
-
else
-
replace(records)
-
end
-
end
-
-
1
def reset
-
2
super
-
2
@target = []
-
2
@association_ids = nil
-
end
-
-
1
def find(*args)
-
if options[:inverse_of] && loaded?
-
args_flatten = args.flatten
-
model = scope.klass
-
-
if args_flatten.blank?
-
error_message = "Couldn't find #{model.name} without an ID"
-
raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
-
end
-
-
result = find_by_scan(*args)
-
-
result_size = Array(result).size
-
if !result || result_size != args_flatten.size
-
scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
-
else
-
result
-
end
-
else
-
scope.find(*args)
-
end
-
end
-
-
1
def build(attributes = nil, &block)
-
if attributes.is_a?(Array)
-
attributes.collect { |attr| build(attr, &block) }
-
else
-
add_to_target(build_record(attributes, &block), replace: true)
-
end
-
end
-
-
# Add +records+ to this association. Since +<<+ flattens its argument list
-
# and inserts each record, +push+ and +concat+ behave identically.
-
1
def concat(*records)
-
records = records.flatten
-
if owner.new_record?
-
load_target
-
concat_records(records)
-
else
-
transaction { concat_records(records) }
-
end
-
end
-
-
# Starts a transaction in the association class's database connection.
-
#
-
# class Author < ActiveRecord::Base
-
# has_many :books
-
# end
-
#
-
# Author.first.books.transaction do
-
# # same effect as calling Book.transaction
-
# end
-
1
def transaction(*args)
-
3
reflection.klass.transaction(*args) do
-
3
yield
-
end
-
end
-
-
# Removes all records from the association without calling callbacks
-
# on the associated records. It honors the +:dependent+ option. However
-
# if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
-
# deletion strategy for the association is applied.
-
#
-
# You can force a particular deletion strategy by passing a parameter.
-
#
-
# Example:
-
#
-
# @author.books.delete_all(:nullify)
-
# @author.books.delete_all(:delete_all)
-
#
-
# See delete for more info.
-
1
def delete_all(dependent = nil)
-
if dependent && ![:nullify, :delete_all].include?(dependent)
-
raise ArgumentError, "Valid values are :nullify or :delete_all"
-
end
-
-
dependent = if dependent
-
dependent
-
elsif options[:dependent] == :destroy
-
:delete_all
-
else
-
options[:dependent]
-
end
-
-
delete_or_nullify_all_records(dependent).tap do
-
reset
-
loaded!
-
end
-
end
-
-
# Destroy all the records from this association.
-
#
-
# See destroy for more info.
-
1
def destroy_all
-
destroy(load_target).tap do
-
reset
-
loaded!
-
end
-
end
-
-
# Removes +records+ from this association calling +before_remove+ and
-
# +after_remove+ callbacks.
-
#
-
# This method is abstract in the sense that +delete_records+ has to be
-
# provided by descendants. Note this method does not imply the records
-
# are actually removed from the database, that depends precisely on
-
# +delete_records+. They are in any case removed from the collection.
-
1
def delete(*records)
-
delete_or_destroy(records, options[:dependent])
-
end
-
-
# Deletes the +records+ and removes them from this association calling
-
# +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
-
#
-
# Note that this method removes records from the database ignoring the
-
# +:dependent+ option.
-
1
def destroy(*records)
-
delete_or_destroy(records, :destroy)
-
end
-
-
# Returns the size of the collection by executing a SELECT COUNT(*)
-
# query if the collection hasn't been loaded, and calling
-
# <tt>collection.size</tt> if it has.
-
#
-
# If the collection has been already loaded +size+ and +length+ are
-
# equivalent. If not and you are going to need the records anyway
-
# +length+ will take one less query. Otherwise +size+ is more efficient.
-
#
-
# This method is abstract in the sense that it relies on
-
# +count_records+, which is a method descendants have to provide.
-
1
def size
-
if !find_target? || loaded?
-
target.size
-
elsif @association_ids
-
@association_ids.size
-
elsif !association_scope.group_values.empty?
-
load_target.size
-
elsif !association_scope.distinct_value && !target.empty?
-
unsaved_records = target.select(&:new_record?)
-
unsaved_records.size + count_records
-
else
-
count_records
-
end
-
end
-
-
# Returns true if the collection is empty.
-
#
-
# If the collection has been loaded
-
# it is equivalent to <tt>collection.size.zero?</tt>. If the
-
# collection has not been loaded, it is equivalent to
-
# <tt>!collection.exists?</tt>. If the collection has not already been
-
# loaded and you are going to fetch the records anyway it is better to
-
# check <tt>collection.length.zero?</tt>.
-
1
def empty?
-
if loaded? || @association_ids || reflection.has_cached_counter?
-
size.zero?
-
else
-
target.empty? && !scope.exists?
-
end
-
end
-
-
# Replace this collection with +other_array+. This will perform a diff
-
# and delete/add only records that have changed.
-
1
def replace(other_array)
-
other_array.each { |val| raise_on_type_mismatch!(val) }
-
original_target = load_target.dup
-
-
if owner.new_record?
-
replace_records(other_array, original_target)
-
else
-
replace_common_records_in_memory(other_array, original_target)
-
if other_array != original_target
-
transaction { replace_records(other_array, original_target) }
-
else
-
other_array
-
end
-
end
-
end
-
-
1
def include?(record)
-
if record.is_a?(reflection.klass)
-
if record.new_record?
-
include_in_memory?(record)
-
else
-
loaded? ? target.include?(record) : scope.exists?(record.id)
-
end
-
else
-
false
-
end
-
end
-
-
1
def load_target
-
if find_target?
-
@target = merge_target_lists(find_target, target)
-
end
-
-
loaded!
-
target
-
end
-
-
1
def add_to_target(record, skip_callbacks: false, replace: false, &block)
-
3
if replace || association_scope.distinct_value
-
index = @target.index(record)
-
end
-
3
replace_on_target(record, index, skip_callbacks, &block)
-
end
-
-
1
def target=(record)
-
return super unless ActiveRecord::Base.has_many_inversing
-
-
case record
-
when Array
-
super
-
else
-
add_to_target(record, skip_callbacks: true, replace: true)
-
end
-
end
-
-
1
def scope
-
6
scope = super
-
6
scope.none! if null_scope?
-
6
scope
-
end
-
-
1
def null_scope?
-
6
owner.new_record? && !foreign_key_present?
-
end
-
-
1
def find_from_target?
-
loaded? ||
-
owner.strict_loading? ||
-
reflection.strict_loading? ||
-
owner.new_record? ||
-
target.any? { |record| record.new_record? || record.changed? }
-
end
-
-
1
private
-
# We have some records loaded from the database (persisted) and some that are
-
# in-memory (memory). The same record may be represented in the persisted array
-
# and in the memory array.
-
#
-
# So the task of this method is to merge them according to the following rules:
-
#
-
# * The final array must not have duplicates
-
# * The order of the persisted array is to be preserved
-
# * Any changes made to attributes on objects in the memory array are to be preserved
-
# * Otherwise, attributes should have the value found in the database
-
1
def merge_target_lists(persisted, memory)
-
return persisted if memory.empty?
-
return memory if persisted.empty?
-
-
persisted.map! do |record|
-
if mem_record = memory.delete(record)
-
-
((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name|
-
mem_record[name] = record[name]
-
end
-
-
mem_record
-
else
-
record
-
end
-
end
-
-
persisted + memory
-
end
-
-
1
def _create_record(attributes, raise = false, &block)
-
3
unless owner.persisted?
-
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
-
end
-
-
3
if attributes.is_a?(Array)
-
attributes.collect { |attr| _create_record(attr, raise, &block) }
-
else
-
3
record = build_record(attributes, &block)
-
3
transaction do
-
3
result = nil
-
3
add_to_target(record) do
-
3
result = insert_record(record, true, raise) {
-
3
@_was_loaded = loaded?
-
}
-
end
-
3
raise ActiveRecord::Rollback unless result
-
end
-
3
record
-
end
-
end
-
-
# Do the relevant stuff to insert the given record into the association collection.
-
1
def insert_record(record, validate = true, raise = false, &block)
-
3
if raise
-
record.save!(validate: validate, &block)
-
else
-
3
record.save(validate: validate, &block)
-
end
-
end
-
-
1
def delete_or_destroy(records, method)
-
return if records.empty?
-
records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
-
records = records.flatten
-
records.each { |record| raise_on_type_mismatch!(record) }
-
existing_records = records.reject(&:new_record?)
-
-
if existing_records.empty?
-
remove_records(existing_records, records, method)
-
else
-
transaction { remove_records(existing_records, records, method) }
-
end
-
end
-
-
1
def remove_records(existing_records, records, method)
-
catch(:abort) do
-
records.each { |record| callback(:before_remove, record) }
-
end || return
-
-
delete_records(existing_records, method) if existing_records.any?
-
@target -= records
-
@association_ids = nil
-
-
records.each { |record| callback(:after_remove, record) }
-
end
-
-
# Delete the given records from the association,
-
# using one of the methods +:destroy+, +:delete_all+
-
# or +:nullify+ (or +nil+, in which case a default is used).
-
1
def delete_records(records, method)
-
raise NotImplementedError
-
end
-
-
1
def replace_records(new_target, original_target)
-
delete(difference(target, new_target))
-
-
unless concat(difference(new_target, target))
-
@target = original_target
-
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
-
"new records could not be saved."
-
end
-
-
target
-
end
-
-
1
def replace_common_records_in_memory(new_target, original_target)
-
common_records = intersection(new_target, original_target)
-
common_records.each do |record|
-
skip_callbacks = true
-
replace_on_target(record, @target.index(record), skip_callbacks)
-
end
-
end
-
-
1
def concat_records(records, raise = false)
-
result = true
-
-
records.each do |record|
-
raise_on_type_mismatch!(record)
-
add_to_target(record) do
-
unless owner.new_record?
-
result &&= insert_record(record, true, raise) {
-
@_was_loaded = loaded?
-
}
-
end
-
end
-
end
-
-
raise ActiveRecord::Rollback unless result
-
-
records
-
end
-
-
1
def replace_on_target(record, index, skip_callbacks)
-
catch(:abort) do
-
3
callback(:before_add, record)
-
3
end || return unless skip_callbacks
-
-
3
set_inverse_instance(record)
-
-
3
@_was_loaded = true
-
-
3
yield(record) if block_given?
-
-
3
if index
-
target[index] = record
-
3
elsif @_was_loaded || !loaded?
-
3
@association_ids = nil
-
3
target << record
-
end
-
-
3
callback(:after_add, record) unless skip_callbacks
-
-
3
record
-
ensure
-
3
@_was_loaded = nil
-
end
-
-
1
def callback(method, record)
-
6
callbacks_for(method).each do |callback|
-
callback.call(method, owner, record)
-
end
-
end
-
-
1
def callbacks_for(callback_name)
-
6
full_callback_name = "#{callback_name}_for_#{reflection.name}"
-
6
owner.class.send(full_callback_name)
-
end
-
-
1
def include_in_memory?(record)
-
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
-
assoc = owner.association(reflection.through_reflection.name)
-
assoc.reader.any? { |source|
-
target_reflection = source.send(reflection.source_reflection.name)
-
target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record
-
} || target.include?(record)
-
else
-
target.include?(record)
-
end
-
end
-
-
# If the :inverse_of option has been
-
# specified, then #find scans the entire collection.
-
1
def find_by_scan(*args)
-
expects_array = args.first.kind_of?(Array)
-
ids = args.flatten.compact.map(&:to_s).uniq
-
-
if ids.size == 1
-
id = ids.first
-
record = load_target.detect { |r| id == r.id.to_s }
-
expects_array ? [ record ] : record
-
else
-
load_target.select { |r| ids.include?(r.id.to_s) }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord::Associations
-
1
module ForeignAssociation # :nodoc:
-
1
def foreign_key_present?
-
if reflection.klass.primary_key
-
owner.attribute_present?(reflection.active_record_primary_key)
-
else
-
false
-
end
-
end
-
-
1
def nullified_owner_attributes
-
Hash.new.tap do |attrs|
-
attrs[reflection.foreign_key] = nil
-
attrs[reflection.type] = nil if reflection.type.present?
-
end
-
end
-
-
1
private
-
# Sets the owner attributes on the given record
-
1
def set_owner_attributes(record)
-
3
return if options[:through]
-
-
3
key = owner._read_attribute(reflection.join_foreign_key)
-
3
record._write_attribute(reflection.join_primary_key, key)
-
-
3
if reflection.type
-
record._write_attribute(reflection.type, owner.class.polymorphic_name)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Associations
-
# = Active Record Has Many Association
-
# This is the proxy that handles a has many association.
-
#
-
# If the association has a <tt>:through</tt> option further specialization
-
# is provided by its child HasManyThroughAssociation.
-
1
class HasManyAssociation < CollectionAssociation #:nodoc:
-
1
include ForeignAssociation
-
-
1
def handle_dependency
-
case options[:dependent]
-
when :restrict_with_exception
-
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
-
-
when :restrict_with_error
-
unless empty?
-
record = owner.class.human_attribute_name(reflection.name).downcase
-
owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record)
-
throw(:abort)
-
end
-
-
when :destroy
-
# No point in executing the counter update since we're going to destroy the parent anyway
-
load_target.each { |t| t.destroyed_by_association = reflection }
-
destroy_all
-
when :destroy_async
-
load_target.each do |t|
-
t.destroyed_by_association = reflection
-
end
-
-
unless target.empty?
-
association_class = target.first.class
-
primary_key_column = association_class.primary_key.to_sym
-
-
ids = target.collect do |assoc|
-
assoc.public_send(primary_key_column)
-
end
-
-
enqueue_destroy_association(
-
owner_model_name: owner.class.to_s,
-
owner_id: owner.id,
-
association_class: reflection.klass.to_s,
-
association_ids: ids,
-
association_primary_key_column: primary_key_column,
-
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
-
)
-
end
-
else
-
delete_all
-
end
-
end
-
-
1
def insert_record(record, validate = true, raise = false)
-
3
set_owner_attributes(record)
-
3
super
-
end
-
-
1
private
-
# Returns the number of records in this collection.
-
#
-
# If the association has a counter cache it gets that value. Otherwise
-
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
-
# there's one. Some configuration options like :group make it impossible
-
# to do an SQL count, in those cases the array count will be used.
-
#
-
# That does not depend on whether the collection has already been loaded
-
# or not. The +size+ method is the one that takes the loaded flag into
-
# account and delegates to +count_records+ if needed.
-
#
-
# If the collection is empty the target is set to an empty array and
-
# the loaded flag is set to true as well.
-
1
def count_records
-
count = if reflection.has_cached_counter?
-
owner.read_attribute(reflection.counter_cache_column).to_i
-
else
-
scope.count(:all)
-
end
-
-
# If there's nothing in the database and @target has no new records
-
# we are certain the current target is an empty array. This is a
-
# documented side-effect of the method that may avoid an extra SELECT.
-
loaded! if count == 0
-
-
[association_scope.limit_value, count].compact.min
-
end
-
-
1
def update_counter(difference, reflection = reflection())
-
if reflection.has_cached_counter?
-
owner.increment!(reflection.counter_cache_column, difference)
-
end
-
end
-
-
1
def update_counter_in_memory(difference, reflection = reflection())
-
3
if reflection.counter_must_be_updated_by_has_many?
-
counter = reflection.counter_cache_column
-
owner.increment(counter, difference)
-
owner.send(:"clear_#{counter}_change")
-
end
-
end
-
-
1
def delete_count(method, scope)
-
if method == :delete_all
-
scope.delete_all
-
else
-
scope.update_all(nullified_owner_attributes)
-
end
-
end
-
-
1
def delete_or_nullify_all_records(method)
-
count = delete_count(method, scope)
-
update_counter(-count)
-
count
-
end
-
-
# Deletes the records according to the <tt>:dependent</tt> option.
-
1
def delete_records(records, method)
-
if method == :destroy
-
records.each(&:destroy!)
-
update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
-
else
-
scope = self.scope.where(reflection.klass.primary_key => records)
-
update_counter(-delete_count(method, scope))
-
end
-
end
-
-
1
def concat_records(records, *)
-
update_counter_if_success(super, records.length)
-
end
-
-
1
def _create_record(attributes, *)
-
3
if attributes.is_a?(Array)
-
super
-
else
-
3
update_counter_if_success(super, 1)
-
end
-
end
-
-
1
def update_counter_if_success(saved_successfully, difference)
-
3
if saved_successfully
-
3
update_counter_in_memory(difference)
-
end
-
3
saved_successfully
-
end
-
-
1
def difference(a, b)
-
a - b
-
end
-
-
1
def intersection(a, b)
-
a & b
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Associations
-
1
class SingularAssociation < Association #:nodoc:
-
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
-
1
def reader
-
6
if !loaded? || stale_target?
-
3
reload
-
end
-
-
6
target
-
end
-
-
# Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
-
1
def writer(record)
-
replace(record)
-
end
-
-
1
def build(attributes = nil, &block)
-
record = build_record(attributes, &block)
-
set_new_record(record)
-
record
-
end
-
-
# Implements the reload reader method, e.g. foo.reload_bar for
-
# Foo.has_one :bar
-
1
def force_reload_reader
-
reload(true)
-
target
-
end
-
-
1
private
-
1
def scope_for_create
-
super.except!(klass.primary_key)
-
end
-
-
1
def find_target
-
3
super.first
-
end
-
-
1
def replace(record)
-
raise NotImplementedError, "Subclasses must implement a replace(record) method"
-
end
-
-
1
def set_new_record(record)
-
replace(record)
-
end
-
-
1
def _create_record(attributes, raise_error = false, &block)
-
record = build_record(attributes, &block)
-
saved = record.save
-
set_new_record(record)
-
raise RecordInvalid.new(record) if !saved && raise_error
-
record
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module ConnectionAdapters
-
1
class TransactionState
-
1
def initialize(state = nil)
-
10
@state = state
-
10
@children = nil
-
end
-
-
1
def add_child(state)
-
8
@children ||= []
-
8
@children << state
-
end
-
-
1
def finalized?
-
@state
-
end
-
-
1
def committed?
-
@state == :committed || @state == :fully_committed
-
end
-
-
1
def fully_committed?
-
@state == :fully_committed
-
end
-
-
1
def rolledback?
-
@state == :rolledback || @state == :fully_rolledback
-
end
-
-
1
def fully_rolledback?
-
@state == :fully_rolledback
-
end
-
-
1
def fully_completed?
-
completed?
-
end
-
-
1
def completed?
-
committed? || rolledback?
-
end
-
-
1
def rollback!
-
8
@children&.each { |c| c.rollback! }
-
8
@state = :rolledback
-
end
-
-
1
def full_rollback!
-
10
@children&.each { |c| c.rollback! }
-
2
@state = :fully_rolledback
-
end
-
-
1
def commit!
-
8
@state = :committed
-
end
-
-
1
def full_commit!
-
@state = :fully_committed
-
end
-
-
1
def nullify!
-
@state = nil
-
end
-
end
-
-
1
class NullTransaction #:nodoc:
-
1
def initialize; end
-
1
def state; end
-
1
def closed?; true; end
-
13
def open?; false; end
-
3
def joinable?; false; end
-
1
def add_record(record, _ = true); end
-
end
-
-
1
class Transaction #:nodoc:
-
1
attr_reader :connection, :state, :savepoint_name, :isolation_level
-
1
attr_accessor :written
-
-
1
def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
-
10
@connection = connection
-
10
@state = TransactionState.new
-
10
@records = nil
-
10
@isolation_level = isolation
-
10
@materialized = false
-
10
@joinable = joinable
-
10
@run_commit_callbacks = run_commit_callbacks
-
10
@lazy_enrollment_records = nil
-
end
-
-
1
def add_record(record, ensure_finalize = true)
-
8
@records ||= []
-
8
if ensure_finalize
-
@records << record
-
else
-
8
@lazy_enrollment_records ||= ObjectSpace::WeakMap.new
-
8
@lazy_enrollment_records[record] = record
-
end
-
end
-
-
1
def records
-
34
if @lazy_enrollment_records
-
8
@records.concat @lazy_enrollment_records.values
-
8
@lazy_enrollment_records = nil
-
end
-
34
@records
-
end
-
-
1
def materialize!
-
10
@materialized = true
-
end
-
-
1
def materialized?
-
26
@materialized
-
end
-
-
1
def rollback_records
-
2
return unless records
-
ite = records.uniq(&:__id__)
-
already_run_callbacks = {}
-
while record = ite.shift
-
trigger_callbacks = record.trigger_transactional_callbacks?
-
should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks
-
already_run_callbacks[record] ||= trigger_callbacks
-
record.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: should_run_callbacks)
-
end
-
ensure
-
2
ite&.each do |i|
-
i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
-
end
-
end
-
-
1
def before_commit_records
-
8
records.uniq.each(&:before_committed!) if records && @run_commit_callbacks
-
end
-
-
1
def commit_records
-
8
return unless records
-
8
ite = records.uniq(&:__id__)
-
8
already_run_callbacks = {}
-
8
while record = ite.shift
-
8
if @run_commit_callbacks
-
8
trigger_callbacks = record.trigger_transactional_callbacks?
-
8
should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks
-
8
already_run_callbacks[record] ||= trigger_callbacks
-
8
record.committed!(should_run_callbacks: should_run_callbacks)
-
else
-
# if not running callbacks, only adds the record to the parent transaction
-
connection.add_transaction_record(record)
-
end
-
end
-
ensure
-
8
ite&.each { |i| i.committed!(should_run_callbacks: false) }
-
end
-
-
1
def full_rollback?; true; end
-
20
def joinable?; @joinable; end
-
46
def closed?; false; end
-
46
def open?; !closed?; end
-
end
-
-
1
class SavepointTransaction < Transaction
-
1
def initialize(connection, savepoint_name, parent_transaction, **options)
-
8
super(connection, **options)
-
-
8
parent_transaction.state.add_child(@state)
-
-
8
if isolation_level
-
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
-
end
-
-
8
@savepoint_name = savepoint_name
-
end
-
-
1
def materialize!
-
8
connection.create_savepoint(savepoint_name)
-
8
super
-
end
-
-
1
def rollback
-
connection.rollback_to_savepoint(savepoint_name) if materialized?
-
@state.rollback!
-
end
-
-
1
def commit
-
8
connection.release_savepoint(savepoint_name) if materialized?
-
8
@state.commit!
-
end
-
-
1
def full_rollback?; false; end
-
end
-
-
1
class RealTransaction < Transaction
-
1
def materialize!
-
2
if isolation_level
-
connection.begin_isolated_db_transaction(isolation_level)
-
else
-
2
connection.begin_db_transaction
-
end
-
-
2
super
-
end
-
-
1
def rollback
-
2
connection.rollback_db_transaction if materialized?
-
2
@state.full_rollback!
-
end
-
-
1
def commit
-
connection.commit_db_transaction if materialized?
-
@state.full_commit!
-
end
-
end
-
-
1
class TransactionManager #:nodoc:
-
1
def initialize(connection)
-
3
@stack = []
-
3
@connection = connection
-
3
@has_unmaterialized_transactions = false
-
3
@materializing_transactions = false
-
3
@lazy_transactions_enabled = true
-
end
-
-
1
def begin_transaction(isolation: nil, joinable: true, _lazy: true)
-
10
@connection.lock.synchronize do
-
10
run_commit_callbacks = !current_transaction.joinable?
-
transaction =
-
10
if @stack.empty?
-
2
RealTransaction.new(
-
@connection,
-
isolation: isolation,
-
joinable: joinable,
-
run_commit_callbacks: run_commit_callbacks
-
)
-
else
-
8
SavepointTransaction.new(
-
@connection,
-
"active_record_#{@stack.size}",
-
@stack.last,
-
isolation: isolation,
-
joinable: joinable,
-
run_commit_callbacks: run_commit_callbacks
-
)
-
end
-
-
10
if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && _lazy
-
8
@has_unmaterialized_transactions = true
-
else
-
2
transaction.materialize!
-
end
-
10
@stack.push(transaction)
-
10
transaction
-
end
-
end
-
-
1
def disable_lazy_transactions!
-
materialize_transactions
-
@lazy_transactions_enabled = false
-
end
-
-
1
def enable_lazy_transactions!
-
3
@lazy_transactions_enabled = true
-
end
-
-
1
def lazy_transactions_enabled?
-
10
@lazy_transactions_enabled
-
end
-
-
1
def materialize_transactions
-
47
return if @materializing_transactions
-
39
return unless @has_unmaterialized_transactions
-
-
8
@connection.lock.synchronize do
-
begin
-
8
@materializing_transactions = true
-
24
@stack.each { |t| t.materialize! unless t.materialized? }
-
ensure
-
8
@materializing_transactions = false
-
end
-
8
@has_unmaterialized_transactions = false
-
end
-
end
-
-
1
def commit_transaction
-
8
@connection.lock.synchronize do
-
8
transaction = @stack.last
-
-
begin
-
8
transaction.before_commit_records
-
ensure
-
8
@stack.pop
-
end
-
-
8
transaction.commit
-
8
transaction.commit_records
-
end
-
end
-
-
1
def rollback_transaction(transaction = nil)
-
2
@connection.lock.synchronize do
-
2
transaction ||= @stack.pop
-
2
transaction.rollback
-
2
transaction.rollback_records
-
end
-
end
-
-
1
def within_new_transaction(isolation: nil, joinable: true)
-
8
@connection.lock.synchronize do
-
8
transaction = begin_transaction(isolation: isolation, joinable: joinable)
-
8
ret = yield
-
8
completed = true
-
8
ret
-
rescue Exception => error
-
if transaction
-
rollback_transaction
-
after_failure_actions(transaction, error)
-
end
-
raise
-
ensure
-
8
if transaction
-
8
if error
-
# @connection still holds an open transaction, so we must not
-
# put it back in the pool for reuse
-
@connection.throw_away! unless transaction.state.rolledback?
-
else
-
8
if Thread.current.status == "aborting"
-
rollback_transaction
-
else
-
8
if !completed && transaction.written
-
ActiveSupport::Deprecation.warn(<<~EOW)
-
Using `return`, `break` or `throw` to exit a transaction block is
-
deprecated without replacement. If the `throw` came from
-
`Timeout.timeout(duration)`, pass an exception class as a second
-
argument so it doesn't use `throw` to abort its block. This results
-
in the transaction being committed, but in the next release of Rails
-
it will rollback.
-
EOW
-
end
-
begin
-
8
commit_transaction
-
rescue Exception
-
rollback_transaction(transaction) unless transaction.state.completed?
-
raise
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
def open_transactions
-
@stack.size
-
end
-
-
1
def current_transaction
-
86
@stack.last || NULL_TRANSACTION
-
end
-
-
1
private
-
1
NULL_TRANSACTION = NullTransaction.new
-
-
# Deallocate invalidated prepared statements outside of the transaction
-
1
def after_failure_actions(transaction, error)
-
return unless transaction.is_a?(RealTransaction)
-
return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired)
-
@connection.clear_cache!
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/configuration_file"
-
-
1
module ActiveRecord
-
1
class FixtureSet
-
1
class File # :nodoc:
-
1
include Enumerable
-
-
##
-
# Open a fixture file named +file+. When called with a block, the block
-
# is called with the filehandle and the filehandle is automatically closed
-
# when the block finishes.
-
1
def self.open(file)
-
x = new file
-
block_given? ? yield(x) : x
-
end
-
-
1
def initialize(file)
-
@file = file
-
end
-
-
1
def each(&block)
-
rows.each(&block)
-
end
-
-
1
def model_class
-
config_row["model_class"]
-
end
-
-
1
def ignored_fixtures
-
config_row["ignore"]
-
end
-
-
1
private
-
1
def rows
-
@rows ||= raw_rows.reject { |fixture_name, _| fixture_name == "_fixture" }
-
end
-
-
1
def config_row
-
@config_row ||= begin
-
row = raw_rows.find { |fixture_name, _| fixture_name == "_fixture" }
-
if row
-
row.last
-
else
-
{ 'model_class': nil, 'ignore': nil }
-
end
-
end
-
end
-
-
1
def raw_rows
-
@raw_rows ||= begin
-
data = ActiveSupport::ConfigurationFile.parse(@file, context:
-
ActiveRecord::FixtureSet::RenderContext.create_subclass.new.get_binding)
-
data ? validate(data).to_a : []
-
rescue RuntimeError => error
-
raise Fixture::FormatError, error.message
-
end
-
end
-
-
# Validate our unmarshalled data.
-
1
def validate(data)
-
unless Hash === data || YAML::Omap === data
-
raise Fixture::FormatError, "fixture is not a hash: #{@file}"
-
end
-
-
invalid = data.reject { |_, row| Hash === row }
-
if invalid.any?
-
raise Fixture::FormatError, "fixture key is not a hash: #{@file}, keys: #{invalid.keys.inspect}"
-
end
-
data
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class FixtureSet
-
1
class ModelMetadata # :nodoc:
-
1
def initialize(model_class)
-
@model_class = model_class
-
end
-
-
1
def primary_key_name
-
@primary_key_name ||= @model_class && @model_class.primary_key
-
end
-
-
1
def primary_key_type
-
@primary_key_type ||= @model_class && @model_class.type_for_attribute(@model_class.primary_key).type
-
end
-
-
1
def has_primary_key_column?
-
@has_primary_key_column ||= primary_key_name &&
-
@model_class.columns.any? { |col| col.name == primary_key_name }
-
end
-
-
1
def timestamp_column_names
-
@model_class.all_timestamp_attributes_in_model
-
end
-
-
1
def inheritance_column_name
-
@inheritance_column_name ||= @model_class && @model_class.inheritance_column
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# NOTE: This class has to be defined in compact style in
-
# order for rendering context subclassing to work correctly.
-
1
class ActiveRecord::FixtureSet::RenderContext # :nodoc:
-
1
def self.create_subclass
-
Class.new(ActiveRecord::FixtureSet.context_class) do
-
def get_binding
-
binding()
-
end
-
-
def binary(path)
-
%(!!binary "#{Base64.strict_encode64(File.binread(path))}")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class FixtureSet
-
1
class TableRow # :nodoc:
-
1
class ReflectionProxy # :nodoc:
-
1
def initialize(association)
-
@association = association
-
end
-
-
1
def join_table
-
@association.join_table
-
end
-
-
1
def name
-
@association.name
-
end
-
-
1
def primary_key_type
-
@association.klass.type_for_attribute(@association.klass.primary_key).type
-
end
-
end
-
-
1
class HasManyThroughProxy < ReflectionProxy # :nodoc:
-
1
def rhs_key
-
@association.foreign_key
-
end
-
-
1
def lhs_key
-
@association.through_reflection.foreign_key
-
end
-
-
1
def join_table
-
@association.through_reflection.table_name
-
end
-
end
-
-
1
def initialize(fixture, table_rows:, label:, now:)
-
@table_rows = table_rows
-
@label = label
-
@now = now
-
@row = fixture.to_hash
-
fill_row_model_attributes
-
end
-
-
1
def to_hash
-
@row
-
end
-
-
1
private
-
1
def model_metadata
-
@table_rows.model_metadata
-
end
-
-
1
def model_class
-
@table_rows.model_class
-
end
-
-
1
def fill_row_model_attributes
-
return unless model_class
-
fill_timestamps
-
interpolate_label
-
generate_primary_key
-
resolve_enums
-
resolve_sti_reflections
-
end
-
-
1
def reflection_class
-
@reflection_class ||= if @row.include?(model_metadata.inheritance_column_name)
-
@row[model_metadata.inheritance_column_name].constantize rescue model_class
-
else
-
model_class
-
end
-
end
-
-
1
def fill_timestamps
-
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
-
if model_class.record_timestamps
-
model_metadata.timestamp_column_names.each do |c_name|
-
@row[c_name] = @now unless @row.key?(c_name)
-
end
-
end
-
end
-
-
1
def interpolate_label
-
# interpolate the fixture label
-
@row.each do |key, value|
-
@row[key] = value.gsub("$LABEL", @label.to_s) if value.is_a?(String)
-
end
-
end
-
-
1
def generate_primary_key
-
# generate a primary key if necessary
-
if model_metadata.has_primary_key_column? && !@row.include?(model_metadata.primary_key_name)
-
@row[model_metadata.primary_key_name] = ActiveRecord::FixtureSet.identify(
-
@label, model_metadata.primary_key_type
-
)
-
end
-
end
-
-
1
def resolve_enums
-
model_class.defined_enums.each do |name, values|
-
if @row.include?(name)
-
@row[name] = values.fetch(@row[name], @row[name])
-
end
-
end
-
end
-
-
1
def resolve_sti_reflections
-
# If STI is used, find the correct subclass for association reflection
-
reflection_class._reflections.each_value do |association|
-
case association.macro
-
when :belongs_to
-
# Do not replace association name with association foreign key if they are named the same
-
fk_name = association.join_foreign_key
-
-
if association.name.to_s != fk_name && value = @row.delete(association.name.to_s)
-
if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
-
# support polymorphic belongs_to as "label (Type)"
-
@row[association.join_foreign_type] = $1
-
end
-
-
fk_type = reflection_class.type_for_attribute(fk_name).type
-
@row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
-
end
-
when :has_many
-
if association.options[:through]
-
add_join_records(HasManyThroughProxy.new(association))
-
end
-
end
-
end
-
end
-
-
1
def add_join_records(association)
-
# This is the case when the join table has no fixtures file
-
if (targets = @row.delete(association.name.to_s))
-
table_name = association.join_table
-
column_type = association.primary_key_type
-
lhs_key = association.lhs_key
-
rhs_key = association.rhs_key
-
-
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
-
joins = targets.map do |target|
-
{ lhs_key => @row[model_metadata.primary_key_name],
-
rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
-
end
-
@table_rows.tables[table_name].concat(joins)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_record/fixture_set/table_row"
-
1
require "active_record/fixture_set/model_metadata"
-
-
1
module ActiveRecord
-
1
class FixtureSet
-
1
class TableRows # :nodoc:
-
1
def initialize(table_name, model_class:, fixtures:, config:)
-
@model_class = model_class
-
-
# track any join tables we need to insert later
-
@tables = Hash.new { |h, table| h[table] = [] }
-
-
# ensure this table is loaded before any HABTM associations
-
@tables[table_name] = nil
-
-
build_table_rows_from(table_name, fixtures, config)
-
end
-
-
1
attr_reader :tables, :model_class
-
-
1
def to_hash
-
@tables.transform_values { |rows| rows.map(&:to_hash) }
-
end
-
-
1
def model_metadata
-
@model_metadata ||= ModelMetadata.new(model_class)
-
end
-
-
1
private
-
1
def build_table_rows_from(table_name, fixtures, config)
-
now = config.default_timezone == :utc ? Time.now.utc : Time.now
-
-
@tables[table_name] = fixtures.map do |label, fixture|
-
TableRow.new(
-
fixture,
-
table_rows: self,
-
label: label,
-
now: now,
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "erb"
-
1
require "yaml"
-
1
require "zlib"
-
1
require "set"
-
1
require "active_support/dependencies"
-
1
require "active_support/core_ext/digest/uuid"
-
1
require "active_record/fixture_set/file"
-
1
require "active_record/fixture_set/render_context"
-
1
require "active_record/fixture_set/table_rows"
-
1
require "active_record/test_fixtures"
-
-
1
module ActiveRecord
-
1
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
-
end
-
-
# \Fixtures are a way of organizing data that you want to test against; in short, sample data.
-
#
-
# They are stored in YAML files, one file per model, which are placed in the directory
-
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
-
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
-
# The fixture file ends with the +.yml+ file extension, for example:
-
# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
-
#
-
# The format of a fixture file looks like this:
-
#
-
# rubyonrails:
-
# id: 1
-
# name: Ruby on Rails
-
# url: http://www.rubyonrails.org
-
#
-
# google:
-
# id: 2
-
# name: Google
-
# url: http://www.google.com
-
#
-
# This fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and
-
# is followed by an indented list of key/value pairs in the "key: value" format. Records are
-
# separated by a blank line for your viewing pleasure.
-
#
-
# Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
-
# See https://yaml.org/type/omap.html
-
# for the specification. You will need ordered fixtures when you have foreign key constraints
-
# on keys in the same table. This is commonly needed for tree structures. Example:
-
#
-
# --- !omap
-
# - parent:
-
# id: 1
-
# parent_id: NULL
-
# title: Parent
-
# - child:
-
# id: 2
-
# parent_id: 1
-
# title: Child
-
#
-
# = Using Fixtures in Test Cases
-
#
-
# Since fixtures are a testing construct, we use them in our unit and functional tests. There
-
# are two ways to use the fixtures, but first let's take a look at a sample unit test:
-
#
-
# require "test_helper"
-
#
-
# class WebSiteTest < ActiveSupport::TestCase
-
# test "web_site_count" do
-
# assert_equal 2, WebSite.count
-
# end
-
# end
-
#
-
# By default, +test_helper.rb+ will load all of your fixtures into your test
-
# database, so this test will succeed.
-
#
-
# The testing environment will automatically load all the fixtures into the database before each
-
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
-
#
-
# In addition to being available in the database, the fixture's data may also be accessed by
-
# using a special dynamic method, which has the same name as the model.
-
#
-
# Passing in a fixture name to this dynamic method returns the fixture matching this name:
-
#
-
# test "find one" do
-
# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
-
# end
-
#
-
# Passing in multiple fixture names returns all fixtures matching these names:
-
#
-
# test "find all by name" do
-
# assert_equal 2, web_sites(:rubyonrails, :google).length
-
# end
-
#
-
# Passing in no arguments returns all fixtures:
-
#
-
# test "find all" do
-
# assert_equal 2, web_sites.length
-
# end
-
#
-
# Passing in any fixture name that does not exist will raise <tt>StandardError</tt>:
-
#
-
# test "find by name that does not exist" do
-
# assert_raise(StandardError) { web_sites(:reddit) }
-
# end
-
#
-
# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the
-
# following tests:
-
#
-
# test "find_alt_method_1" do
-
# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
-
# end
-
#
-
# test "find_alt_method_2" do
-
# assert_equal "Ruby on Rails", @rubyonrails.name
-
# end
-
#
-
# In order to use these methods to access fixtured data within your test cases, you must specify one of the
-
# following in your ActiveSupport::TestCase-derived class:
-
#
-
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
-
# self.use_instantiated_fixtures = true
-
#
-
# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
-
# self.use_instantiated_fixtures = :no_instances
-
#
-
# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
-
# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
-
# large sets of fixtured data.
-
#
-
# = Dynamic fixtures with ERB
-
#
-
# Sometimes you don't care about the content of the fixtures as much as you care about the volume.
-
# In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load
-
# testing, like:
-
#
-
# <% 1.upto(1000) do |i| %>
-
# fix_<%= i %>:
-
# id: <%= i %>
-
# name: guy_<%= i %>
-
# <% end %>
-
#
-
# This will create 1000 very simple fixtures.
-
#
-
# Using ERB, you can also inject dynamic values into your fixtures with inserts like
-
# <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
-
# This is however a feature to be used with some caution. The point of fixtures are that they're
-
# stable units of predictable sample data. If you feel that you need to inject dynamic values, then
-
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
-
# in fixtures are to be considered a code smell.
-
#
-
# Helper methods defined in a fixture will not be available in other fixtures, to prevent against
-
# unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
-
# that is included in ActiveRecord::FixtureSet.context_class.
-
#
-
# - define a helper method in <tt>test_helper.rb</tt>
-
# module FixtureFileHelpers
-
# def file_sha(path)
-
# Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
-
# end
-
# end
-
# ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
-
#
-
# - use the helper method in a fixture
-
# photo:
-
# name: kitten.png
-
# sha: <%= file_sha 'files/kitten.png' %>
-
#
-
# = Transactional Tests
-
#
-
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
-
# delete+insert for every test case.
-
#
-
# class FooTest < ActiveSupport::TestCase
-
# self.use_transactional_tests = true
-
#
-
# test "godzilla" do
-
# assert_not_empty Foo.all
-
# Foo.destroy_all
-
# assert_empty Foo.all
-
# end
-
#
-
# test "godzilla aftermath" do
-
# assert_not_empty Foo.all
-
# end
-
# end
-
#
-
# If you preload your test database with all fixture data (probably by running <tt>bin/rails db:fixtures:load</tt>)
-
# and use transactional tests, then you may omit all fixtures declarations in your test cases since
-
# all the data's already there and every case rolls back its changes.
-
#
-
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
-
# true. This will provide access to fixture data for every table that has been loaded through
-
# fixtures (depending on the value of +use_instantiated_fixtures+).
-
#
-
# When *not* to use transactional tests:
-
#
-
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until
-
# all parent transactions commit, particularly, the fixtures transaction which is begun in setup
-
# and rolled back in teardown. Thus, you won't be able to verify
-
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
-
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
-
# Use InnoDB, MaxDB, or NDB instead.
-
#
-
# = Advanced Fixtures
-
#
-
# Fixtures that don't specify an ID get some extra features:
-
#
-
# * Stable, autogenerated IDs
-
# * Label references for associations (belongs_to, has_one, has_many)
-
# * HABTM associations as inline lists
-
#
-
# There are some more advanced features available even if the id is specified:
-
#
-
# * Autofilled timestamp columns
-
# * Fixture label interpolation
-
# * Support for YAML defaults
-
#
-
# == Stable, Autogenerated IDs
-
#
-
# Here, have a monkey fixture:
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
#
-
# reginald:
-
# id: 2
-
# name: Reginald the Pirate
-
#
-
# Each of these fixtures has two unique identifiers: one for the database
-
# and one for the humans. Why don't we generate the primary key instead?
-
# Hashing each fixture's label yields a consistent ID:
-
#
-
# george: # generated id: 503576764
-
# name: George the Monkey
-
#
-
# reginald: # generated id: 324201669
-
# name: Reginald the Pirate
-
#
-
# Active Record looks at the fixture's model class, discovers the correct
-
# primary key, and generates it right before inserting the fixture
-
# into the database.
-
#
-
# The generated ID for a given label is constant, so we can discover
-
# any fixture's ID without loading anything, as long as we know the label.
-
#
-
# == Label references for associations (belongs_to, has_one, has_many)
-
#
-
# Specifying foreign keys in fixtures can be very fragile, not to
-
# mention difficult to read. Since Active Record can figure out the ID of
-
# any fixture from its label, you can specify FK's by label instead of ID.
-
#
-
# === belongs_to
-
#
-
# Let's break out some more monkeys and pirates.
-
#
-
# ### in pirates.yml
-
#
-
# reginald:
-
# id: 1
-
# name: Reginald the Pirate
-
# monkey_id: 1
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
# pirate_id: 1
-
#
-
# Add a few more monkeys and pirates and break this into multiple files,
-
# and it gets pretty hard to keep track of what's going on. Let's
-
# use labels instead of IDs:
-
#
-
# ### in pirates.yml
-
#
-
# reginald:
-
# name: Reginald the Pirate
-
# monkey: george
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# name: George the Monkey
-
# pirate: reginald
-
#
-
# Pow! All is made clear. Active Record reflects on the fixture's model class,
-
# finds all the +belongs_to+ associations, and allows you to specify
-
# a target *label* for the *association* (monkey: george) rather than
-
# a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
-
#
-
# ==== Polymorphic belongs_to
-
#
-
# Supporting polymorphic relationships is a little bit more complicated, since
-
# Active Record needs to know what type your association is pointing at. Something
-
# like this should look familiar:
-
#
-
# ### in fruit.rb
-
#
-
# belongs_to :eater, polymorphic: true
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# id: 1
-
# name: apple
-
# eater_id: 1
-
# eater_type: Monkey
-
#
-
# Can we do better? You bet!
-
#
-
# apple:
-
# eater: george (Monkey)
-
#
-
# Just provide the polymorphic target type and Active Record will take care of the rest.
-
#
-
# === has_and_belongs_to_many
-
#
-
# Time to give our monkey some fruit.
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# id: 1
-
# name: apple
-
#
-
# orange:
-
# id: 2
-
# name: orange
-
#
-
# grape:
-
# id: 3
-
# name: grape
-
#
-
# ### in fruits_monkeys.yml
-
#
-
# apple_george:
-
# fruit_id: 1
-
# monkey_id: 1
-
#
-
# orange_george:
-
# fruit_id: 2
-
# monkey_id: 1
-
#
-
# grape_george:
-
# fruit_id: 3
-
# monkey_id: 1
-
#
-
# Let's make the HABTM fixture go away.
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
# fruits: apple, orange, grape
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# name: apple
-
#
-
# orange:
-
# name: orange
-
#
-
# grape:
-
# name: grape
-
#
-
# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
-
# on George's fixture, but we could've just as easily specified a list
-
# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
-
# the fixture's model class and discovers the +has_and_belongs_to_many+
-
# associations.
-
#
-
# == Autofilled Timestamp Columns
-
#
-
# If your table/model specifies any of Active Record's
-
# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
-
# they will automatically be set to <tt>Time.now</tt>.
-
#
-
# If you've set specific values, they'll be left alone.
-
#
-
# == Fixture label interpolation
-
#
-
# The label of the current fixture is always available as a column value:
-
#
-
# geeksomnia:
-
# name: Geeksomnia's Account
-
# subdomain: $LABEL
-
# email: $LABEL@email.com
-
#
-
# Also, sometimes (like when porting older join table fixtures) you'll need
-
# to be able to get a hold of the identifier for a given label. ERB
-
# to the rescue:
-
#
-
# george_reginald:
-
# monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
-
# pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>
-
#
-
# == Support for YAML defaults
-
#
-
# You can set and reuse defaults in your fixtures YAML file.
-
# This is the same technique used in the +database.yml+ file to specify
-
# defaults:
-
#
-
# DEFAULTS: &DEFAULTS
-
# created_on: <%= 3.weeks.ago.to_s(:db) %>
-
#
-
# first:
-
# name: Smurf
-
# <<: *DEFAULTS
-
#
-
# second:
-
# name: Fraggle
-
# <<: *DEFAULTS
-
#
-
# Any fixture labeled "DEFAULTS" is safely ignored.
-
#
-
# Besides using "DEFAULTS", you can also specify what fixtures will
-
# be ignored by setting "ignore" in "_fixture" section.
-
#
-
# # users.yml
-
# _fixture:
-
# ignore:
-
# - base
-
# # or use "ignore: base" when there is only one fixture needs to be ignored.
-
#
-
# base: &base
-
# admin: false
-
# introduction: "This is a default description"
-
#
-
# admin:
-
# <<: *base
-
# admin: true
-
#
-
# visitor:
-
# <<: *base
-
#
-
# In the above example, 'base' will be ignored when creating fixtures.
-
# This can be used for common attributes inheriting.
-
#
-
# == Configure the fixture model class
-
#
-
# It's possible to set the fixture's model class directly in the YAML file.
-
# This is helpful when fixtures are loaded outside tests and
-
# +set_fixture_class+ is not available (e.g.
-
# when running <tt>bin/rails db:fixtures:load</tt>).
-
#
-
# _fixture:
-
# model_class: User
-
# david:
-
# name: David
-
#
-
# Any fixtures labeled "_fixture" are safely ignored.
-
1
class FixtureSet
-
#--
-
# An instance of FixtureSet is normally stored in a single YAML file and
-
# possibly in a folder with the same name.
-
#++
-
-
1
MAX_ID = 2**30 - 1
-
-
2
@@all_cached_fixtures = Hash.new { |h, k| h[k] = {} }
-
-
1
cattr_accessor :all_loaded_fixtures, default: {}
-
-
1
class ClassCache
-
1
def initialize(class_names, config)
-
2
@class_names = class_names.stringify_keys
-
2
@config = config
-
-
# Remove string values that aren't constants or subclasses of AR
-
2
@class_names.delete_if do |klass_name, klass|
-
!insert_class(@class_names, klass_name, klass)
-
end
-
end
-
-
1
def [](fs_name)
-
@class_names.fetch(fs_name) do
-
klass = default_fixture_model(fs_name, @config).safe_constantize
-
insert_class(@class_names, fs_name, klass)
-
end
-
end
-
-
1
private
-
1
def insert_class(class_names, name, klass)
-
# We only want to deal with AR objects.
-
if klass && klass < ActiveRecord::Base
-
class_names[name] = klass
-
else
-
class_names[name] = nil
-
end
-
end
-
-
1
def default_fixture_model(fs_name, config)
-
ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
-
end
-
end
-
-
1
class << self
-
1
def default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
-
config.pluralize_table_names ?
-
fixture_set_name.singularize.camelize :
-
fixture_set_name.camelize
-
end
-
-
1
def default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
-
"#{ config.table_name_prefix }"\
-
"#{ fixture_set_name.tr('/', '_') }"\
-
"#{ config.table_name_suffix }".to_sym
-
end
-
-
1
def reset_cache
-
@@all_cached_fixtures.clear
-
end
-
-
1
def cache_for_connection(connection)
-
2
@@all_cached_fixtures[connection]
-
end
-
-
1
def fixture_is_cached?(connection, table_name)
-
cache_for_connection(connection)[table_name]
-
end
-
-
1
def cached_fixtures(connection, keys_to_fetch = nil)
-
2
if keys_to_fetch
-
2
cache_for_connection(connection).values_at(*keys_to_fetch)
-
else
-
cache_for_connection(connection).values
-
end
-
end
-
-
1
def cache_fixtures(connection, fixtures_map)
-
cache_for_connection(connection).update(fixtures_map)
-
end
-
-
1
def instantiate_fixtures(object, fixture_set, load_instances = true)
-
return unless load_instances
-
fixture_set.each do |fixture_name, fixture|
-
object.instance_variable_set "@#{fixture_name}", fixture.find
-
rescue FixtureClassNotFound
-
nil
-
end
-
end
-
-
1
def instantiate_all_loaded_fixtures(object, load_instances = true)
-
all_loaded_fixtures.each_value do |fixture_set|
-
instantiate_fixtures(object, fixture_set, load_instances)
-
end
-
end
-
-
1
def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base, &block)
-
2
fixture_set_names = Array(fixture_set_names).map(&:to_s)
-
2
class_names = ClassCache.new class_names, config
-
-
# FIXME: Apparently JK uses this.
-
4
connection = block_given? ? block : lambda { ActiveRecord::Base.connection }
-
-
2
fixture_files_to_read = fixture_set_names.reject do |fs_name|
-
fixture_is_cached?(connection.call, fs_name)
-
end
-
-
2
if fixture_files_to_read.any?
-
fixtures_map = read_and_insert(
-
fixtures_directory,
-
fixture_files_to_read,
-
class_names,
-
connection,
-
)
-
cache_fixtures(connection.call, fixtures_map)
-
end
-
2
cached_fixtures(connection.call, fixture_set_names)
-
end
-
-
# Returns a consistent, platform-independent identifier for +label+.
-
# Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
-
1
def identify(label, column_type = :integer)
-
if column_type == :uuid
-
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
-
else
-
Zlib.crc32(label.to_s) % MAX_ID
-
end
-
end
-
-
1
def signed_global_id(fixture_set_name, label, column_type: :integer, **options)
-
identifier = identify(label, column_type)
-
model_name = default_fixture_model_name(fixture_set_name)
-
uri = URI::GID.build([GlobalID.app, model_name, identifier, {}])
-
-
SignedGlobalID.new(uri, **options)
-
end
-
-
# Superclass for the evaluation contexts used by ERB fixtures.
-
1
def context_class
-
@context_class ||= Class.new
-
end
-
-
1
private
-
1
def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc:
-
fixtures_map = {}
-
fixture_sets = fixture_files.map do |fixture_set_name|
-
klass = class_names[fixture_set_name]
-
fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new
-
nil,
-
fixture_set_name,
-
klass,
-
::File.join(fixtures_directory, fixture_set_name)
-
)
-
end
-
update_all_loaded_fixtures(fixtures_map)
-
-
insert(fixture_sets, connection)
-
-
fixtures_map
-
end
-
-
1
def insert(fixture_sets, connection) # :nodoc:
-
fixture_sets_by_connection = fixture_sets.group_by do |fixture_set|
-
if fixture_set.model_class
-
fixture_set.model_class.connection
-
else
-
connection.call
-
end
-
end
-
-
fixture_sets_by_connection.each do |conn, set|
-
table_rows_for_connection = Hash.new { |h, k| h[k] = [] }
-
-
set.each do |fixture_set|
-
fixture_set.table_rows.each do |table, rows|
-
table_rows_for_connection[table].unshift(*rows)
-
end
-
end
-
-
conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys)
-
-
# Cap primary key sequences to max(pk).
-
if conn.respond_to?(:reset_pk_sequence!)
-
set.each { |fs| conn.reset_pk_sequence!(fs.table_name) }
-
end
-
end
-
end
-
-
1
def update_all_loaded_fixtures(fixtures_map) # :nodoc:
-
all_loaded_fixtures.update(fixtures_map)
-
end
-
end
-
-
1
attr_reader :table_name, :name, :fixtures, :model_class, :ignored_fixtures, :config
-
-
1
def initialize(_, name, class_name, path, config = ActiveRecord::Base)
-
@name = name
-
@path = path
-
@config = config
-
-
self.model_class = class_name
-
-
@fixtures = read_fixture_files(path)
-
-
@table_name = model_class&.table_name || self.class.default_fixture_table_name(name, config)
-
end
-
-
1
def [](x)
-
fixtures[x]
-
end
-
-
1
def []=(k, v)
-
fixtures[k] = v
-
end
-
-
1
def each(&block)
-
fixtures.each(&block)
-
end
-
-
1
def size
-
fixtures.size
-
end
-
-
# Returns a hash of rows to be inserted. The key is the table, the value is
-
# a list of rows to insert to that table.
-
1
def table_rows
-
# allow specifying fixtures to be ignored by setting `ignore` in `_fixture` section
-
fixtures.except!(*ignored_fixtures)
-
-
TableRows.new(
-
table_name,
-
model_class: model_class,
-
fixtures: fixtures,
-
config: config,
-
).to_hash
-
end
-
-
1
private
-
1
def model_class=(class_name)
-
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
-
@model_class = class_name
-
else
-
@model_class = class_name.safe_constantize if class_name
-
end
-
end
-
-
1
def ignored_fixtures=(base)
-
@ignored_fixtures =
-
case base
-
when Array
-
base
-
when String
-
[base]
-
else
-
[]
-
end
-
-
@ignored_fixtures << "DEFAULTS" unless @ignored_fixtures.include?("DEFAULTS")
-
@ignored_fixtures.compact
-
end
-
-
# Loads the fixtures from the YAML file at +path+.
-
# If the file sets the +model_class+ and current instance value is not set,
-
# it uses the file value.
-
1
def read_fixture_files(path)
-
yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
-
::File.file?(f)
-
} + [yaml_file_path(path)]
-
-
yaml_files.each_with_object({}) do |file, fixtures|
-
FixtureSet::File.open(file) do |fh|
-
self.model_class ||= fh.model_class if fh.model_class
-
self.ignored_fixtures ||= fh.ignored_fixtures
-
fh.each do |fixture_name, row|
-
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
-
end
-
end
-
end
-
end
-
-
1
def yaml_file_path(path)
-
"#{path}.yml"
-
end
-
end
-
-
1
class Fixture #:nodoc:
-
1
include Enumerable
-
-
1
class FixtureError < StandardError #:nodoc:
-
end
-
-
1
class FormatError < FixtureError #:nodoc:
-
end
-
-
1
attr_reader :model_class, :fixture
-
-
1
def initialize(fixture, model_class)
-
@fixture = fixture
-
@model_class = model_class
-
end
-
-
1
def class_name
-
model_class.name if model_class
-
end
-
-
1
def each
-
fixture.each { |item| yield item }
-
end
-
-
1
def [](key)
-
fixture[key]
-
end
-
-
1
alias :to_hash :fixture
-
-
1
def find
-
raise FixtureClassNotFound, "No class attached to find." unless model_class
-
object = model_class.unscoped do
-
model_class.find(fixture[model_class.primary_key])
-
end
-
# Fixtures can't be eagerly loaded
-
object.instance_variable_set(:@strict_loading, false)
-
object
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_record/scoping/default"
-
1
require "active_record/scoping/named"
-
-
1
module ActiveRecord
-
# This class is used to create a table that keeps track of values and keys such
-
# as which environment migrations were run in.
-
#
-
# This is enabled by default. To disable this functionality set
-
# `use_metadata_table` to false in your database configuration.
-
1
class InternalMetadata < ActiveRecord::Base # :nodoc:
-
1
class << self
-
1
def enabled?
-
2
ActiveRecord::Base.connection.use_metadata_table?
-
end
-
-
1
def _internal?
-
true
-
end
-
-
1
def primary_key
-
"key"
-
end
-
-
1
def table_name
-
4
"#{table_name_prefix}#{internal_metadata_table_name}#{table_name_suffix}"
-
end
-
-
1
def []=(key, value)
-
return unless enabled?
-
-
find_or_initialize_by(key: key).update!(value: value)
-
end
-
-
1
def [](key)
-
1
return unless enabled?
-
-
1
where(key: key).pluck(:value).first
-
end
-
-
# Creates an internal metadata table with columns +key+ and +value+
-
1
def create_table
-
return unless enabled?
-
-
unless connection.table_exists?(table_name)
-
connection.create_table(table_name, id: false) do |t|
-
t.string :key, **connection.internal_string_options_for_primary_key
-
t.string :value
-
t.timestamps
-
end
-
end
-
end
-
-
1
def drop_table
-
return unless enabled?
-
-
connection.drop_table table_name, if_exists: true
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder # :nodoc:
-
1
require "active_record/relation/predicate_builder/array_handler"
-
1
require "active_record/relation/predicate_builder/basic_object_handler"
-
1
require "active_record/relation/predicate_builder/range_handler"
-
1
require "active_record/relation/predicate_builder/relation_handler"
-
1
require "active_record/relation/predicate_builder/association_query_value"
-
1
require "active_record/relation/predicate_builder/polymorphic_array_value"
-
-
# No-op BaseHandler to work Mashal.load(File.read("legacy_relation.dump")).
-
# TODO: Remove the constant alias once Rails 6.1 has released.
-
1
BaseHandler = BasicObjectHandler
-
-
1
def initialize(table)
-
4
@table = table
-
4
@handlers = []
-
-
4
register_handler(BasicObject, BasicObjectHandler.new(self))
-
4
register_handler(Range, RangeHandler.new(self))
-
4
register_handler(Relation, RelationHandler.new)
-
4
register_handler(Array, ArrayHandler.new(self))
-
4
register_handler(Set, ArrayHandler.new(self))
-
end
-
-
1
def build_from_hash(attributes, &block)
-
10
attributes = convert_dot_notation_to_hash(attributes)
-
10
expand_from_hash(attributes, &block)
-
end
-
-
1
def self.references(attributes)
-
10
attributes.each_with_object([]) do |(key, value), result|
-
10
if value.is_a?(Hash)
-
result << Arel.sql(key)
-
10
elsif key.include?(".")
-
result << Arel.sql(key.split(".").first)
-
end
-
end
-
end
-
-
# Define how a class is converted to Arel nodes when passed to +where+.
-
# The handler can be any object that responds to +call+, and will be used
-
# for any value that +===+ the class given. For example:
-
#
-
# MyCustomDateRange = Struct.new(:start, :end)
-
# handler = proc do |column, range|
-
# Arel::Nodes::Between.new(column,
-
# Arel::Nodes::And.new([range.start, range.end])
-
# )
-
# end
-
# ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
-
1
def register_handler(klass, handler)
-
20
@handlers.unshift([klass, handler])
-
end
-
-
1
def [](attr_name, value, operator = nil)
-
10
build(table.arel_table[attr_name], value, operator)
-
end
-
-
1
def build(attribute, value, operator = nil)
-
10
value = value.id if value.respond_to?(:id)
-
10
if operator ||= table.type(attribute.name).force_equality?(value) && :eq
-
bind = build_bind_attribute(attribute.name, value)
-
attribute.public_send(operator, bind)
-
else
-
10
handler_for(value).call(attribute, value)
-
end
-
end
-
-
1
def build_bind_attribute(column_name, value)
-
49
attr = Relation::QueryAttribute.new(column_name, value, table.type(column_name))
-
49
Arel::Nodes::BindParam.new(attr)
-
end
-
-
1
def resolve_arel_attribute(table_name, column_name, &block)
-
table.associated_table(table_name, &block).arel_table[column_name]
-
end
-
-
1
protected
-
1
def expand_from_hash(attributes, &block)
-
10
return ["1=0"] if attributes.empty?
-
-
10
attributes.flat_map do |key, value|
-
10
if value.is_a?(Hash) && !table.has_column?(key)
-
table.associated_table(key, &block)
-
.predicate_builder.expand_from_hash(value.stringify_keys)
-
10
elsif table.associated_with?(key)
-
# Find the foreign key when using queries such as:
-
# Post.where(author: author)
-
#
-
# For polymorphic relationships, find the foreign key and type:
-
# PriceEstimate.where(estimate_of: treasure)
-
associated_table = table.associated_table(key)
-
if associated_table.polymorphic_association?
-
value = [value] unless value.is_a?(Array)
-
klass = PolymorphicArrayValue
-
elsif associated_table.through_association?
-
next associated_table.predicate_builder.expand_from_hash(
-
associated_table.primary_key => value
-
)
-
end
-
-
klass ||= AssociationQueryValue
-
queries = klass.new(associated_table, value).queries.map! do |query|
-
# If the query produced is identical to attributes don't go any deeper.
-
# Prevents stack level too deep errors when association and foreign_key are identical.
-
query == attributes ? self[key, value] : expand_from_hash(query)
-
end
-
-
grouping_queries(queries)
-
10
elsif table.aggregated_with?(key)
-
mapping = table.reflect_on_aggregation(key).mapping
-
values = value.nil? ? [nil] : Array.wrap(value)
-
if mapping.length == 1 || values.empty?
-
column_name, aggr_attr = mapping.first
-
values = values.map do |object|
-
object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
-
end
-
self[column_name, values]
-
else
-
queries = values.map do |object|
-
mapping.map do |field_attr, aggregate_attr|
-
self[field_attr, object.try!(aggregate_attr)]
-
end
-
end
-
-
grouping_queries(queries)
-
end
-
else
-
10
self[key, value]
-
end
-
end
-
end
-
-
1
private
-
1
attr_reader :table
-
-
1
def grouping_queries(queries)
-
if queries.one?
-
queries.first
-
else
-
queries.map! { |query| query.reduce(&:and) }
-
queries = queries.reduce { |result, query| Arel::Nodes::Or.new(result, query) }
-
Arel::Nodes::Grouping.new(queries)
-
end
-
end
-
-
1
def convert_dot_notation_to_hash(attributes)
-
10
dot_notation = attributes.select do |k, v|
-
10
k.include?(".") && !v.is_a?(Hash)
-
end
-
-
10
dot_notation.each_key do |key|
-
table_name, column_name = key.split(".")
-
value = attributes.delete(key)
-
attributes[table_name] ||= {}
-
-
attributes[table_name] = attributes[table_name].merge(column_name => value)
-
end
-
-
10
attributes
-
end
-
-
1
def handler_for(object)
-
60
@handlers.detect { |klass, _| klass === object }.last
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/array/extract"
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class ArrayHandler # :nodoc:
-
1
def initialize(predicate_builder)
-
8
@predicate_builder = predicate_builder
-
end
-
-
1
def call(attribute, value)
-
return attribute.in([]) if value.empty?
-
-
values = value.map { |x| x.is_a?(Base) ? x.id : x }
-
nils = values.extract!(&:nil?)
-
ranges = values.extract! { |v| v.is_a?(Range) }
-
-
values_predicate =
-
case values.length
-
when 0 then NullPredicate
-
when 1 then predicate_builder.build(attribute, values.first)
-
else Arel::Nodes::HomogeneousIn.new(values, attribute, :in)
-
end
-
-
unless nils.empty?
-
values_predicate = values_predicate.or(attribute.eq(nil))
-
end
-
-
if ranges.empty?
-
values_predicate
-
else
-
array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) }
-
array_predicates.inject(values_predicate, &:or)
-
end
-
end
-
-
1
private
-
1
attr_reader :predicate_builder
-
-
1
module NullPredicate # :nodoc:
-
1
def self.or(other)
-
other
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class AssociationQueryValue # :nodoc:
-
1
def initialize(associated_table, value)
-
@associated_table = associated_table
-
@value = value
-
end
-
-
1
def queries
-
[ associated_table.join_foreign_key => ids ]
-
end
-
-
1
private
-
1
attr_reader :associated_table, :value
-
-
1
def ids
-
case value
-
when Relation
-
value.select_values.empty? ? value.select(primary_key) : value
-
when Array
-
value.map { |v| convert_to_id(v) }
-
else
-
convert_to_id(value)
-
end
-
end
-
-
1
def primary_key
-
associated_table.join_primary_key
-
end
-
-
1
def convert_to_id(value)
-
if value.respond_to?(primary_key)
-
value.public_send(primary_key)
-
else
-
value
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class BasicObjectHandler # :nodoc:
-
1
def initialize(predicate_builder)
-
4
@predicate_builder = predicate_builder
-
end
-
-
1
def call(attribute, value)
-
10
bind = predicate_builder.build_bind_attribute(attribute.name, value)
-
10
attribute.eq(bind)
-
end
-
-
1
private
-
1
attr_reader :predicate_builder
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class PolymorphicArrayValue # :nodoc:
-
1
def initialize(associated_table, values)
-
@associated_table = associated_table
-
@values = values
-
end
-
-
1
def queries
-
return [ associated_table.join_foreign_key => values ] if values.empty?
-
-
type_to_ids_mapping.map do |type, ids|
-
query = {}
-
query[associated_table.join_foreign_type] = type if type
-
query[associated_table.join_foreign_key] = ids
-
query
-
end
-
end
-
-
1
private
-
1
attr_reader :associated_table, :values
-
-
1
def type_to_ids_mapping
-
default_hash = Hash.new { |hsh, key| hsh[key] = [] }
-
values.each_with_object(default_hash) do |value, hash|
-
hash[klass(value)&.polymorphic_name] << convert_to_id(value)
-
end
-
end
-
-
1
def primary_key(value)
-
associated_table.join_primary_key(klass(value))
-
end
-
-
1
def klass(value)
-
case value
-
when Base
-
value.class
-
when Relation
-
value.klass
-
end
-
end
-
-
1
def convert_to_id(value)
-
case value
-
when Base
-
value._read_attribute(primary_key(value))
-
when Relation
-
value.select(primary_key(value))
-
else
-
value
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class RangeHandler # :nodoc:
-
1
RangeWithBinds = Struct.new(:begin, :end, :exclude_end?)
-
-
1
def initialize(predicate_builder)
-
4
@predicate_builder = predicate_builder
-
end
-
-
1
def call(attribute, value)
-
begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin)
-
end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end)
-
attribute.between(RangeWithBinds.new(begin_bind, end_bind, value.exclude_end?))
-
end
-
-
1
private
-
1
attr_reader :predicate_builder
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class RelationHandler # :nodoc:
-
1
def call(attribute, value)
-
if value.eager_loading?
-
value = value.send(:apply_join_dependency)
-
end
-
-
if value.select_values.empty?
-
value = value.select(value.table[value.klass.primary_key])
-
end
-
-
attribute.in(value.arel)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
###
-
# This class encapsulates a result returned from calling
-
# {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query]
-
# on any database connection adapter. For example:
-
#
-
# result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
-
# result # => #<ActiveRecord::Result:0xdeadbeef>
-
#
-
# # Get the column names of the result:
-
# result.columns
-
# # => ["id", "title", "body"]
-
#
-
# # Get the record values of the result:
-
# result.rows
-
# # => [[1, "title_1", "body_1"],
-
# [2, "title_2", "body_2"],
-
# ...
-
# ]
-
#
-
# # Get an array of hashes representing the result (column => value):
-
# result.to_a
-
# # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
-
# {"id" => 2, "title" => "title_2", "body" => "body_2"},
-
# ...
-
# ]
-
#
-
# # ActiveRecord::Result also includes Enumerable.
-
# result.each do |row|
-
# puts row['title'] + " " + row['body']
-
# end
-
1
class Result
-
1
include Enumerable
-
-
1
attr_reader :columns, :rows, :column_types
-
-
1
def initialize(columns, rows, column_types = {})
-
17
@columns = columns
-
17
@rows = rows
-
17
@hash_rows = nil
-
17
@column_types = column_types
-
end
-
-
# Returns true if this result set includes the column named +name+
-
1
def includes_column?(name)
-
6
@columns.include? name
-
end
-
-
# Returns the number of elements in the rows array.
-
1
def length
-
6
@rows.length
-
end
-
-
# Calls the given block once for each element in row collection, passing
-
# row as parameter.
-
#
-
# Returns an +Enumerator+ if no block is given.
-
1
def each
-
6
if block_given?
-
9
hash_rows.each { |row| yield row }
-
else
-
hash_rows.to_enum { @rows.size }
-
end
-
end
-
-
1
alias :map! :map
-
1
alias :collect! :map
-
1
deprecate "map!": :map
-
1
deprecate "collect!": :map
-
-
# Returns true if there are no records, otherwise false.
-
1
def empty?
-
rows.empty?
-
end
-
-
# Returns an array of hashes representing each row record.
-
1
def to_ary
-
hash_rows
-
end
-
-
1
alias :to_a :to_ary
-
-
1
def [](idx)
-
hash_rows[idx]
-
end
-
-
# Returns the last record from the rows collection.
-
1
def last(n = nil)
-
n ? hash_rows.last(n) : hash_rows.last
-
end
-
-
1
def cast_values(type_overrides = {}) # :nodoc:
-
3
if columns.one?
-
# Separated to avoid allocating an array per row
-
-
3
type = if type_overrides.is_a?(Array)
-
2
type_overrides.first
-
else
-
1
column_type(columns.first, type_overrides)
-
end
-
-
3
rows.map do |(value)|
-
13
type.deserialize(value)
-
end
-
else
-
types = if type_overrides.is_a?(Array)
-
type_overrides
-
else
-
columns.map { |name| column_type(name, type_overrides) }
-
end
-
-
rows.map do |values|
-
Array.new(values.size) { |i| types[i].deserialize(values[i]) }
-
end
-
end
-
end
-
-
1
def initialize_copy(other)
-
@columns = columns.dup
-
@rows = rows.dup
-
@column_types = column_types.dup
-
@hash_rows = nil
-
end
-
-
1
private
-
1
def column_type(name, type_overrides = {})
-
1
type_overrides.fetch(name) do
-
1
column_types.fetch(name, Type.default_value)
-
end
-
end
-
-
1
def hash_rows
-
6
@hash_rows ||=
-
begin
-
# We freeze the strings to prevent them getting duped when
-
# used as keys in ActiveRecord::Base's @attributes hash
-
6
columns = @columns.map(&:-@)
-
6
length = columns.length
-
6
template = nil
-
-
6
@rows.map { |row|
-
3
if template
-
# We use transform_values to build subsequent rows from the
-
# hash of the first row. This is faster because we avoid any
-
# reallocs and in Ruby 2.7+ avoid hashing entirely.
-
index = -1
-
template.transform_values do
-
row[index += 1]
-
end
-
else
-
# In the past we used Hash[columns.zip(row)]
-
# though elegant, the verbose way is much more efficient
-
# both time and memory wise cause it avoids a big array allocation
-
# this method is called a lot and needs to be micro optimised
-
3
hash = {}
-
-
3
index = 0
-
3
while index < length
-
21
hash[columns[index]] = row[index]
-
21
index += 1
-
end
-
-
# It's possible to select the same column twice, in which case
-
# we can't use a template
-
3
template = hash if hash.length == length
-
-
3
hash
-
end
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/per_thread_registry"
-
-
1
module ActiveRecord
-
# This is a thread locals registry for Active Record. For example:
-
#
-
# ActiveRecord::RuntimeRegistry.connection_handler
-
#
-
# returns the connection handler local to the current thread.
-
#
-
# See the documentation of ActiveSupport::PerThreadRegistry
-
# for further details.
-
1
class RuntimeRegistry # :nodoc:
-
1
extend ActiveSupport::PerThreadRegistry
-
-
1
attr_accessor :sql_runtime
-
-
1
[:sql_runtime].each do |val|
-
1
class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
-
1
class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_record/scoping/default"
-
1
require "active_record/scoping/named"
-
-
1
module ActiveRecord
-
# This class is used to create a table that keeps track of which migrations
-
# have been applied to a given database. When a migration is run, its schema
-
# number is inserted in to the `SchemaMigration.table_name` so it doesn't need
-
# to be executed the next time.
-
1
class SchemaMigration < ActiveRecord::Base # :nodoc:
-
1
class << self
-
1
def _internal?
-
true
-
end
-
-
1
def primary_key
-
"version"
-
end
-
-
1
def table_name
-
4
"#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
-
end
-
-
1
def create_table
-
unless connection.table_exists?(table_name)
-
connection.create_table(table_name, id: false) do |t|
-
t.string :version, **connection.internal_string_options_for_primary_key
-
end
-
end
-
end
-
-
1
def drop_table
-
connection.drop_table table_name, if_exists: true
-
end
-
-
1
def normalize_migration_number(number)
-
"%.3d" % number.to_i
-
end
-
-
1
def normalized_versions
-
all_versions.map { |v| normalize_migration_number v }
-
end
-
-
1
def all_versions
-
1
order(:version).pluck(:version)
-
end
-
end
-
-
1
def version
-
super.to_i
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
# Statement cache is used to cache a single statement in order to avoid creating the AST again.
-
# Initializing the cache is done by passing the statement in the create block:
-
#
-
# cache = StatementCache.create(Book.connection) do |params|
-
# Book.where(name: "my book").where("author_id > 3")
-
# end
-
#
-
# The cached statement is executed by using the
-
# {connection.execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] method:
-
#
-
# cache.execute([], Book.connection)
-
#
-
# The relation returned by the block is cached, and for each
-
# {execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute]
-
# call the cached relation gets duped. Database is queried when +to_a+ is called on the relation.
-
#
-
# If you want to cache the statement without the values you can use the +bind+ method of the
-
# block parameter.
-
#
-
# cache = StatementCache.create(Book.connection) do |params|
-
# Book.where(name: params.bind)
-
# end
-
#
-
# And pass the bind values as the first argument of +execute+ call.
-
#
-
# cache.execute(["my book"], Book.connection)
-
1
class StatementCache # :nodoc:
-
1
class Substitute; end # :nodoc:
-
-
1
class Query # :nodoc:
-
1
def initialize(sql)
-
@sql = sql
-
end
-
-
1
def sql_for(binds, connection)
-
@sql
-
end
-
end
-
-
1
class PartialQuery < Query # :nodoc:
-
1
def initialize(values)
-
1
@values = values
-
1
@indexes = values.each_with_index.find_all { |thing, i|
-
16
Substitute === thing
-
}.map(&:last)
-
end
-
-
1
def sql_for(binds, connection)
-
3
val = @values.dup
-
3
@indexes.each do |i|
-
6
value = binds.shift
-
6
if ActiveModel::Attribute === value
-
6
value = value.value_for_database
-
end
-
6
val[i] = connection.quote(value)
-
end
-
3
val.join
-
end
-
end
-
-
1
class PartialQueryCollector
-
1
attr_accessor :preparable
-
-
1
def initialize
-
1
@parts = []
-
1
@binds = []
-
end
-
-
1
def <<(str)
-
14
@parts << str
-
14
self
-
end
-
-
1
def add_bind(obj)
-
2
@binds << obj
-
2
@parts << Substitute.new
-
2
self
-
end
-
-
1
def add_binds(binds)
-
@binds.concat binds
-
binds.size.times do |i|
-
@parts << ", " unless i == 0
-
@parts << Substitute.new
-
end
-
self
-
end
-
-
1
def value
-
1
[@parts, @binds]
-
end
-
end
-
-
1
def self.query(sql)
-
Query.new(sql)
-
end
-
-
1
def self.partial_query(values)
-
1
PartialQuery.new(values)
-
end
-
-
1
def self.partial_query_collector
-
1
PartialQueryCollector.new
-
end
-
-
1
class Params # :nodoc:
-
2
def bind; Substitute.new; end
-
end
-
-
1
class BindMap # :nodoc:
-
1
def initialize(bound_attributes)
-
1
@indexes = []
-
1
@bound_attributes = bound_attributes
-
-
1
bound_attributes.each_with_index do |attr, i|
-
2
if ActiveModel::Attribute === attr && Substitute === attr.value
-
1
@indexes << i
-
end
-
end
-
end
-
-
1
def bind(values)
-
3
bas = @bound_attributes.dup
-
6
@indexes.each_with_index { |offset, i| bas[offset] = bas[offset].with_cast_value(values[i]) }
-
3
bas
-
end
-
end
-
-
1
def self.create(connection, callable = nil, &block)
-
1
relation = (callable || block).call Params.new
-
1
query_builder, binds = connection.cacheable_query(self, relation.arel)
-
1
bind_map = BindMap.new(binds)
-
1
new(query_builder, bind_map, relation.klass)
-
end
-
-
1
def initialize(query_builder, bind_map, klass)
-
1
@query_builder = query_builder
-
1
@bind_map = bind_map
-
1
@klass = klass
-
end
-
-
1
def execute(params, connection, &block)
-
3
bind_values = bind_map.bind params
-
-
3
sql = query_builder.sql_for bind_values, connection
-
-
3
klass.find_by_sql(sql, bind_values, preparable: true, &block)
-
rescue ::RangeError
-
[]
-
end
-
-
1
def self.unsupported_value?(value)
-
case value
-
when NilClass, Array, Range, Hash, Relation, Base then true
-
end
-
end
-
-
1
private
-
1
attr_reader :query_builder, :bind_map, :klass
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class TableMetadata # :nodoc:
-
1
delegate :join_primary_key, :join_foreign_key, :join_foreign_type, to: :reflection
-
-
1
def initialize(klass, arel_table, reflection = nil)
-
4
@klass = klass
-
4
@arel_table = arel_table
-
4
@reflection = reflection
-
end
-
-
1
def primary_key
-
klass&.primary_key
-
end
-
-
1
def type(column_name)
-
59
arel_table.type_for_attribute(column_name)
-
end
-
-
1
def has_column?(column_name)
-
klass&.columns_hash.key?(column_name)
-
end
-
-
1
def associated_with?(table_name)
-
10
klass&._reflect_on_association(table_name) || klass&._reflect_on_association(table_name.singularize)
-
end
-
-
1
def associated_table(table_name)
-
reflection = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize)
-
-
if !reflection && table_name == arel_table.name
-
return self
-
end
-
-
if reflection
-
association_klass = reflection.klass unless reflection.polymorphic?
-
elsif block_given?
-
association_klass = yield table_name
-
end
-
-
if association_klass
-
arel_table = association_klass.arel_table
-
arel_table = arel_table.alias(table_name) if arel_table.name != table_name
-
TableMetadata.new(association_klass, arel_table, reflection)
-
else
-
type_caster = TypeCaster::Connection.new(klass, table_name)
-
arel_table = Arel::Table.new(table_name, type_caster: type_caster)
-
TableMetadata.new(nil, arel_table, reflection)
-
end
-
end
-
-
1
def polymorphic_association?
-
reflection&.polymorphic?
-
end
-
-
1
def through_association?
-
reflection&.through_reflection?
-
end
-
-
1
def reflect_on_aggregation(aggregation_name)
-
10
klass&.reflect_on_aggregation(aggregation_name)
-
end
-
1
alias :aggregated_with? :reflect_on_aggregation
-
-
1
def predicate_builder
-
if klass
-
predicate_builder = klass.predicate_builder.dup
-
predicate_builder.instance_variable_set(:@table, self)
-
predicate_builder
-
else
-
PredicateBuilder.new(self)
-
end
-
end
-
-
1
attr_reader :arel_table
-
-
1
private
-
1
attr_reader :klass, :reflection
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/enumerable"
-
-
1
module ActiveRecord
-
1
module TestFixtures
-
1
extend ActiveSupport::Concern
-
-
1
def before_setup # :nodoc:
-
2
setup_fixtures
-
2
super
-
end
-
-
1
def after_teardown # :nodoc:
-
2
super
-
2
teardown_fixtures
-
end
-
-
1
included do
-
1
class_attribute :fixture_path, instance_writer: false
-
1
class_attribute :fixture_table_names, default: []
-
1
class_attribute :fixture_class_names, default: {}
-
1
class_attribute :use_transactional_tests, default: true
-
1
class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
-
1
class_attribute :pre_loaded_fixtures, default: false
-
1
class_attribute :lock_threads, default: true
-
end
-
-
1
module ClassMethods
-
# Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
-
#
-
# Examples:
-
#
-
# set_fixture_class some_fixture: SomeModel,
-
# 'namespaced/fixture' => Another::Model
-
#
-
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
-
1
def set_fixture_class(class_names = {})
-
self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys)
-
end
-
-
1
def fixtures(*fixture_set_names)
-
if fixture_set_names.first == :all
-
raise StandardError, "No fixture path found. Please set `#{self}.fixture_path`." if fixture_path.blank?
-
fixture_set_names = Dir[::File.join(fixture_path, "{**,*}/*.{yml}")].uniq
-
fixture_set_names.reject! { |f| f.start_with?(file_fixture_path.to_s) } if defined?(file_fixture_path) && file_fixture_path
-
fixture_set_names.map! { |f| f[fixture_path.to_s.size..-5].delete_prefix("/") }
-
else
-
fixture_set_names = fixture_set_names.flatten.map(&:to_s)
-
end
-
-
self.fixture_table_names |= fixture_set_names
-
setup_fixture_accessors(fixture_set_names)
-
end
-
-
1
def setup_fixture_accessors(fixture_set_names = nil)
-
fixture_set_names = Array(fixture_set_names || fixture_table_names)
-
methods = Module.new do
-
fixture_set_names.each do |fs_name|
-
fs_name = fs_name.to_s
-
accessor_name = fs_name.tr("/", "_").to_sym
-
-
define_method(accessor_name) do |*fixture_names|
-
force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
-
return_single_record = fixture_names.size == 1
-
fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty?
-
-
@fixture_cache[fs_name] ||= {}
-
-
instances = fixture_names.map do |f_name|
-
f_name = f_name.to_s if f_name.is_a?(Symbol)
-
@fixture_cache[fs_name].delete(f_name) if force_reload
-
-
if @loaded_fixtures[fs_name][f_name]
-
@fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
-
else
-
raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
-
end
-
end
-
-
return_single_record ? instances.first : instances
-
end
-
private accessor_name
-
end
-
end
-
include methods
-
end
-
-
1
def uses_transaction(*methods)
-
@uses_transaction = [] unless defined?(@uses_transaction)
-
@uses_transaction.concat methods.map(&:to_s)
-
end
-
-
1
def uses_transaction?(method)
-
4
@uses_transaction = [] unless defined?(@uses_transaction)
-
4
@uses_transaction.include?(method.to_s)
-
end
-
end
-
-
1
def run_in_transaction?
-
4
use_transactional_tests &&
-
!self.class.uses_transaction?(name)
-
end
-
-
1
def setup_fixtures(config = ActiveRecord::Base)
-
2
if pre_loaded_fixtures && !use_transactional_tests
-
raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
-
end
-
-
2
@fixture_cache = {}
-
2
@fixture_connections = []
-
2
@@already_loaded_fixtures ||= {}
-
2
@connection_subscriber = nil
-
-
# Load fixtures once and begin transaction.
-
2
if run_in_transaction?
-
2
if @@already_loaded_fixtures[self.class]
-
@loaded_fixtures = @@already_loaded_fixtures[self.class]
-
else
-
2
@loaded_fixtures = load_fixtures(config)
-
2
@@already_loaded_fixtures[self.class] = @loaded_fixtures
-
end
-
-
# Begin transactions for connections already established
-
2
@fixture_connections = enlist_fixture_connections
-
2
@fixture_connections.each do |connection|
-
2
connection.begin_transaction joinable: false, _lazy: false
-
2
connection.pool.lock_thread = true if lock_threads
-
end
-
-
# When connections are established in the future, begin a transaction too
-
2
@connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
-
spec_name = payload[:spec_name] if payload.key?(:spec_name)
-
shard = payload[:shard] if payload.key?(:shard)
-
setup_shared_connection_pool
-
-
if spec_name
-
begin
-
connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
-
rescue ConnectionNotEstablished
-
connection = nil
-
end
-
-
if connection && !@fixture_connections.include?(connection)
-
connection.begin_transaction joinable: false, _lazy: false
-
connection.pool.lock_thread = true if lock_threads
-
@fixture_connections << connection
-
end
-
end
-
end
-
-
# Load fixtures for every test.
-
else
-
ActiveRecord::FixtureSet.reset_cache
-
@@already_loaded_fixtures[self.class] = nil
-
@loaded_fixtures = load_fixtures(config)
-
end
-
-
# Instantiate fixtures for every test if requested.
-
2
instantiate_fixtures if use_instantiated_fixtures
-
end
-
-
1
def teardown_fixtures
-
# Rollback changes if a transaction is active.
-
2
if run_in_transaction?
-
2
ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
-
2
@fixture_connections.each do |connection|
-
2
connection.rollback_transaction if connection.transaction_open?
-
2
connection.pool.lock_thread = false
-
end
-
2
@fixture_connections.clear
-
else
-
ActiveRecord::FixtureSet.reset_cache
-
end
-
-
2
ActiveRecord::Base.clear_active_connections!
-
end
-
-
1
def enlist_fixture_connections
-
2
setup_shared_connection_pool
-
-
2
ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
-
end
-
-
1
private
-
# Shares the writing connection pool with connections on
-
# other handlers.
-
#
-
# In an application with a primary and replica the test fixtures
-
# need to share a connection pool so that the reading connection
-
# can see data in the open transaction on the writing connection.
-
1
def setup_shared_connection_pool
-
2
if ActiveRecord::Base.legacy_connection_handling
-
writing_handler = ActiveRecord::Base.connection_handlers[ActiveRecord::Base.writing_role]
-
-
ActiveRecord::Base.connection_handlers.values.each do |handler|
-
if handler != writing_handler
-
handler.connection_pool_names.each do |name|
-
writing_pool_manager = writing_handler.send(:owner_to_pool_manager)[name]
-
return unless writing_pool_manager
-
-
pool_manager = handler.send(:owner_to_pool_manager)[name]
-
pool_manager.shard_names.each do |shard_name|
-
writing_pool_config = writing_pool_manager.get_pool_config(nil, shard_name)
-
pool_manager.set_pool_config(nil, shard_name, writing_pool_config)
-
end
-
end
-
end
-
end
-
else
-
2
handler = ActiveRecord::Base.connection_handler
-
-
2
handler.connection_pool_names.each do |name|
-
2
pool_manager = handler.send(:owner_to_pool_manager)[name]
-
2
pool_manager.shard_names.each do |shard_name|
-
2
writing_pool_config = pool_manager.get_pool_config(ActiveRecord::Base.writing_role, shard_name)
-
2
pool_manager.role_names.each do |role|
-
2
next unless pool_manager.get_pool_config(role, shard_name)
-
2
pool_manager.set_pool_config(role, shard_name, writing_pool_config)
-
end
-
end
-
end
-
end
-
end
-
-
1
def load_fixtures(config)
-
2
ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config).index_by(&:name)
-
end
-
-
1
def instantiate_fixtures
-
if pre_loaded_fixtures
-
raise RuntimeError, "Load fixtures before instantiating them." if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
-
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
-
else
-
raise RuntimeError, "Load fixtures before instantiating them." if @loaded_fixtures.nil?
-
@loaded_fixtures.each_value do |fixture_set|
-
ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?)
-
end
-
end
-
end
-
-
1
def load_instances?
-
use_instantiated_fixtures != :no_instances
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/concern"
-
1
require "active_support/ordered_options"
-
-
1
module ActiveSupport
-
# Configurable provides a <tt>config</tt> method to store and retrieve
-
# configuration options as an <tt>OrderedOptions</tt>.
-
1
module Configurable
-
1
extend ActiveSupport::Concern
-
-
1
class Configuration < ActiveSupport::InheritableOptions
-
1
def compile_methods!
-
2
self.class.compile_methods!(keys)
-
end
-
-
# Compiles reader methods so we don't have to go through method_missing.
-
1
def self.compile_methods!(keys)
-
30
keys.reject { |m| method_defined?(m) }.each do |key|
-
18
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{key}; _get(#{key.inspect}); end
-
RUBY
-
end
-
end
-
end
-
-
1
module ClassMethods
-
1
def config
-
51
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
-
3
superclass.config.inheritable_copy
-
else
-
# create a new "anonymous" class that will host the compiled reader methods
-
1
Class.new(Configuration).new
-
end
-
end
-
-
1
def configure
-
yield config
-
end
-
-
# Allows you to add shortcut so that you don't have to refer to attribute
-
# through config. Also look at the example for config to contrast.
-
#
-
# Defines both class and instance config accessors.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :allowed_access
-
# end
-
#
-
# User.allowed_access # => nil
-
# User.allowed_access = false
-
# User.allowed_access # => false
-
#
-
# user = User.new
-
# user.allowed_access # => false
-
# user.allowed_access = true
-
# user.allowed_access # => true
-
#
-
# User.allowed_access # => false
-
#
-
# The attribute name must be a valid method name in Ruby.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :"1_Badname"
-
# end
-
# # => NameError: invalid config attribute name
-
#
-
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
-
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :allowed_access, instance_reader: false, instance_writer: false
-
# end
-
#
-
# User.allowed_access = false
-
# User.allowed_access # => false
-
#
-
# User.new.allowed_access = true # => NoMethodError
-
# User.new.allowed_access # => NoMethodError
-
#
-
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :allowed_access, instance_accessor: false
-
# end
-
#
-
# User.allowed_access = false
-
# User.allowed_access # => false
-
#
-
# User.new.allowed_access = true # => NoMethodError
-
# User.new.allowed_access # => NoMethodError
-
#
-
# Also you can pass a block to set up the attribute with a default value.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :hair_colors do
-
# [:brown, :black, :blonde, :red]
-
# end
-
# end
-
#
-
# User.hair_colors # => [:brown, :black, :blonde, :red]
-
1
def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true) # :doc:
-
18
names.each do |name|
-
28
raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name)
-
-
28
reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
-
28
writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
-
-
28
singleton_class.class_eval reader, __FILE__, reader_line
-
28
singleton_class.class_eval writer, __FILE__, writer_line
-
-
28
if instance_accessor
-
28
class_eval reader, __FILE__, reader_line if instance_reader
-
28
class_eval writer, __FILE__, writer_line if instance_writer
-
end
-
28
send("#{name}=", yield) if block_given?
-
end
-
end
-
1
private :config_accessor
-
end
-
-
# Reads and writes attributes from a configuration <tt>OrderedOptions</tt>.
-
#
-
# require "active_support/configurable"
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# end
-
#
-
# user = User.new
-
#
-
# user.config.allowed_access = true
-
# user.config.level = 1
-
#
-
# user.config.allowed_access # => true
-
# user.config.level # => 1
-
1
def config
-
@_config ||= self.class.config.inheritable_copy
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport::CurrentAttributes::TestHelper # :nodoc:
-
1
def before_setup
-
ActiveSupport::CurrentAttributes.reset_all
-
super
-
end
-
-
1
def after_teardown
-
super
-
ActiveSupport::CurrentAttributes.reset_all
-
end
-
end
-
# frozen_string_literal: true
-
-
1
gem "minitest" # make sure we get the gem, not stdlib
-
1
require "minitest"
-
1
require "active_support/testing/tagged_logging"
-
1
require "active_support/testing/setup_and_teardown"
-
1
require "active_support/testing/assertions"
-
1
require "active_support/testing/deprecation"
-
1
require "active_support/testing/declarative"
-
1
require "active_support/testing/isolation"
-
1
require "active_support/testing/constant_lookup"
-
1
require "active_support/testing/time_helpers"
-
1
require "active_support/testing/file_fixtures"
-
1
require "active_support/testing/parallelization"
-
1
require "concurrent/utility/processor_counter"
-
-
1
module ActiveSupport
-
1
class TestCase < ::Minitest::Test
-
1
Assertion = Minitest::Assertion
-
-
1
class << self
-
# Sets the order in which test cases are run.
-
#
-
# ActiveSupport::TestCase.test_order = :random # => :random
-
#
-
# Valid values are:
-
# * +:random+ (to run tests in random order)
-
# * +:parallel+ (to run tests in parallel)
-
# * +:sorted+ (to run tests alphabetically by method name)
-
# * +:alpha+ (equivalent to +:sorted+)
-
1
def test_order=(new_order)
-
ActiveSupport.test_order = new_order
-
end
-
-
# Returns the order in which test cases are run.
-
#
-
# ActiveSupport::TestCase.test_order # => :random
-
#
-
# Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
-
# Defaults to +:random+.
-
1
def test_order
-
ActiveSupport.test_order ||= :random
-
end
-
-
# Parallelizes the test suite.
-
#
-
# Takes a +workers+ argument that controls how many times the process
-
# is forked. For each process a new database will be created suffixed
-
# with the worker number.
-
#
-
# test-database-0
-
# test-database-1
-
#
-
# If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
-
# and the environment variable will be used instead. This is useful for CI
-
# environments, or other environments where you may need more workers than
-
# you do for local testing.
-
#
-
# If the number of workers is set to +1+ or fewer, the tests will not be
-
# parallelized.
-
#
-
# If +workers+ is set to +:number_of_processors+, the number of workers will be
-
# set to the actual core count on the machine you are on.
-
#
-
# The default parallelization method is to fork processes. If you'd like to
-
# use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+
-
# method. Note the threaded parallelization does not create multiple
-
# database and will not work with system tests at this time.
-
#
-
# parallelize(workers: :number_of_processors, with: :threads)
-
#
-
# The threaded parallelization uses minitest's parallel executor directly.
-
# The processes parallelization uses a Ruby DRb server.
-
1
def parallelize(workers: :number_of_processors, with: :processes)
-
workers = Concurrent.physical_processor_count if workers == :number_of_processors
-
workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
-
-
return if workers <= 1
-
-
executor = case with
-
when :processes
-
Testing::Parallelization.new(workers)
-
when :threads
-
Minitest::Parallel::Executor.new(workers)
-
else
-
raise ArgumentError, "#{with} is not a supported parallelization executor."
-
end
-
-
self.lock_threads = false if defined?(self.lock_threads) && with == :threads
-
-
Minitest.parallel_executor = executor
-
-
parallelize_me!
-
end
-
-
# Set up hook for parallel testing. This can be used if you have multiple
-
# databases or any behavior that needs to be run after the process is forked
-
# but before the tests run.
-
#
-
# Note: this feature is not available with the threaded parallelization.
-
#
-
# In your +test_helper.rb+ add the following:
-
#
-
# class ActiveSupport::TestCase
-
# parallelize_setup do
-
# # create databases
-
# end
-
# end
-
1
def parallelize_setup(&block)
-
ActiveSupport::Testing::Parallelization.after_fork_hook do |worker|
-
yield worker
-
end
-
end
-
-
# Clean up hook for parallel testing. This can be used to drop databases
-
# if your app uses multiple write/read databases or other clean up before
-
# the tests finish. This runs before the forked process is closed.
-
#
-
# Note: this feature is not available with the threaded parallelization.
-
#
-
# In your +test_helper.rb+ add the following:
-
#
-
# class ActiveSupport::TestCase
-
# parallelize_teardown do
-
# # drop databases
-
# end
-
# end
-
1
def parallelize_teardown(&block)
-
ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker|
-
yield worker
-
end
-
end
-
end
-
-
1
alias_method :method_name, :name
-
-
1
include ActiveSupport::Testing::TaggedLogging
-
1
prepend ActiveSupport::Testing::SetupAndTeardown
-
1
include ActiveSupport::Testing::Assertions
-
1
include ActiveSupport::Testing::Deprecation
-
1
include ActiveSupport::Testing::TimeHelpers
-
1
include ActiveSupport::Testing::FileFixtures
-
1
extend ActiveSupport::Testing::Declarative
-
-
# test/unit backwards compatibility methods
-
1
alias :assert_raise :assert_raises
-
1
alias :assert_not_empty :refute_empty
-
1
alias :assert_not_equal :refute_equal
-
1
alias :assert_not_in_delta :refute_in_delta
-
1
alias :assert_not_in_epsilon :refute_in_epsilon
-
1
alias :assert_not_includes :refute_includes
-
1
alias :assert_not_instance_of :refute_instance_of
-
1
alias :assert_not_kind_of :refute_kind_of
-
1
alias :assert_no_match :refute_match
-
1
alias :assert_not_nil :refute_nil
-
1
alias :assert_not_operator :refute_operator
-
1
alias :assert_not_predicate :refute_predicate
-
1
alias :assert_not_respond_to :refute_respond_to
-
1
alias :assert_not_same :refute_same
-
-
1
ActiveSupport.run_load_hooks(:active_support_test_case, self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/enumerable"
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Assertions
-
1
UNTRACKED = Object.new # :nodoc:
-
-
# Asserts that an expression is not truthy. Passes if <tt>object</tt> is
-
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
-
# like <tt>if foo</tt>.
-
#
-
# assert_not nil # => true
-
# assert_not false # => true
-
# assert_not 'foo' # => Expected "foo" to be nil or false
-
#
-
# An error message can be specified.
-
#
-
# assert_not foo, 'foo should be false'
-
1
def assert_not(object, message = nil)
-
message ||= "Expected #{mu_pp(object)} to be nil or false"
-
assert !object, message
-
end
-
-
# Assertion that the block should not raise an exception.
-
#
-
# Passes if evaluated code in the yielded block raises no exception.
-
#
-
# assert_nothing_raised do
-
# perform_service(param: 'no_exception')
-
# end
-
1
def assert_nothing_raised
-
yield
-
rescue => error
-
raise Minitest::UnexpectedError.new(error)
-
end
-
-
# Test numeric difference between the return value of an expression as a
-
# result of what is evaluated in the yielded block.
-
#
-
# assert_difference 'Article.count' do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# An arbitrary expression is passed in and evaluated.
-
#
-
# assert_difference 'Article.last.comments(:reload).size' do
-
# post :create, params: { comment: {...} }
-
# end
-
#
-
# An arbitrary positive or negative difference can be specified.
-
# The default is <tt>1</tt>.
-
#
-
# assert_difference 'Article.count', -1 do
-
# post :delete, params: { id: ... }
-
# end
-
#
-
# An array of expressions can also be passed in and evaluated.
-
#
-
# assert_difference [ 'Article.count', 'Post.count' ], 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# A hash of expressions/numeric differences can also be passed in and evaluated.
-
#
-
# assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# A lambda or a list of lambdas can be passed in and evaluated:
-
#
-
# assert_difference ->{ Article.count }, 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
-
# post :delete, params: { id: ... }
-
# end
-
1
def assert_difference(expression, *args, &block)
-
expressions =
-
if expression.is_a?(Hash)
-
message = args[0]
-
expression
-
else
-
difference = args[0] || 1
-
message = args[1]
-
Array(expression).index_with(difference)
-
end
-
-
exps = expressions.keys.map { |e|
-
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
-
}
-
before = exps.map(&:call)
-
-
retval = assert_nothing_raised(&block)
-
-
expressions.zip(exps, before) do |(code, diff), exp, before_value|
-
error = "#{code.inspect} didn't change by #{diff}"
-
error = "#{message}.\n#{error}" if message
-
assert_equal(before_value + diff, exp.call, error)
-
end
-
-
retval
-
end
-
-
# Assertion that the numeric result of evaluating an expression is not
-
# changed before and after invoking the passed in block.
-
#
-
# assert_no_difference 'Article.count' do
-
# post :create, params: { article: invalid_attributes }
-
# end
-
#
-
# A lambda can be passed in and evaluated.
-
#
-
# assert_no_difference -> { Article.count } do
-
# post :create, params: { article: invalid_attributes }
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_no_difference 'Article.count', 'An Article should not be created' do
-
# post :create, params: { article: invalid_attributes }
-
# end
-
#
-
# An array of expressions can also be passed in and evaluated.
-
#
-
# assert_no_difference [ 'Article.count', -> { Post.count } ] do
-
# post :create, params: { article: invalid_attributes }
-
# end
-
1
def assert_no_difference(expression, message = nil, &block)
-
assert_difference expression, 0, message, &block
-
end
-
-
# Assertion that the result of evaluating an expression is changed before
-
# and after invoking the passed in block.
-
#
-
# assert_changes 'Status.all_good?' do
-
# post :create, params: { status: { ok: false } }
-
# end
-
#
-
# You can pass the block as a string to be evaluated in the context of
-
# the block. A lambda can be passed for the block as well.
-
#
-
# assert_changes -> { Status.all_good? } do
-
# post :create, params: { status: { ok: false } }
-
# end
-
#
-
# The assertion is useful to test side effects. The passed block can be
-
# anything that can be converted to string with #to_s.
-
#
-
# assert_changes :@object do
-
# @object = 42
-
# end
-
#
-
# The keyword arguments :from and :to can be given to specify the
-
# expected initial value and the expected value after the block was
-
# executed.
-
#
-
# assert_changes :@object, from: nil, to: :foo do
-
# @object = :foo
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
-
# post :create, params: { status: { incident: true } }
-
# end
-
1
def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
-
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
-
-
before = exp.call
-
retval = assert_nothing_raised(&block)
-
-
unless from == UNTRACKED
-
error = "Expected change from #{from.inspect}"
-
error = "#{message}.\n#{error}" if message
-
assert from === before, error
-
end
-
-
after = exp.call
-
-
error = "#{expression.inspect} didn't change"
-
error = "#{error}. It was already #{to}" if before == to
-
error = "#{message}.\n#{error}" if message
-
assert_not_equal before, after, error
-
-
unless to == UNTRACKED
-
error = "Expected change to #{to}\n"
-
error = "#{message}.\n#{error}" if message
-
assert to === after, error
-
end
-
-
retval
-
end
-
-
# Assertion that the result of evaluating an expression is not changed before
-
# and after invoking the passed in block.
-
#
-
# assert_no_changes 'Status.all_good?' do
-
# post :create, params: { status: { ok: true } }
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
-
# post :create, params: { status: { ok: false } }
-
# end
-
1
def assert_no_changes(expression, message = nil, &block)
-
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
-
-
before = exp.call
-
retval = assert_nothing_raised(&block)
-
after = exp.call
-
-
error = "#{expression.inspect} changed"
-
error = "#{message}.\n#{error}" if message
-
-
if before.nil?
-
assert_nil after, error
-
else
-
assert_equal before, after, error
-
end
-
-
retval
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/concern"
-
1
require "active_support/inflector"
-
-
1
module ActiveSupport
-
1
module Testing
-
# Resolves a constant from a minitest spec name.
-
#
-
# Given the following spec-style test:
-
#
-
# describe WidgetsController, :index do
-
# describe "authenticated user" do
-
# describe "returns widgets" do
-
# it "has a controller that exists" do
-
# assert_kind_of WidgetsController, @controller
-
# end
-
# end
-
# end
-
# end
-
#
-
# The test will have the following name:
-
#
-
# "WidgetsController::index::authenticated user::returns widgets"
-
#
-
# The constant WidgetsController can be resolved from the name.
-
# The following code will resolve the constant:
-
#
-
# controller = determine_constant_from_test_name(name) do |constant|
-
# Class === constant && constant < ::ActionController::Metal
-
# end
-
1
module ConstantLookup
-
1
extend ::ActiveSupport::Concern
-
-
1
module ClassMethods # :nodoc:
-
1
def determine_constant_from_test_name(test_name)
-
names = test_name.split "::"
-
while names.size > 0 do
-
names.last.sub!(/Test$/, "")
-
begin
-
constant = names.join("::").safe_constantize
-
break(constant) if yield(constant)
-
ensure
-
names.pop
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Declarative
-
1
unless defined?(Spec)
-
# Helper to define a test method using a String. Under the hood, it replaces
-
# spaces with underscores and defines the test method.
-
#
-
# test "verify something" do
-
# ...
-
# end
-
1
def test(name, &block)
-
test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
-
defined = method_defined? test_name
-
raise "#{test_name} is already defined in #{self}" if defined
-
if block_given?
-
define_method(test_name, &block)
-
else
-
define_method(test_name) do
-
flunk "No implementation provided for #{name}"
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/deprecation"
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Deprecation #:nodoc:
-
1
def assert_deprecated(match = nil, deprecator = nil, &block)
-
result, warnings = collect_deprecations(deprecator, &block)
-
assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
-
if match
-
match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
-
assert warnings.any? { |w| match.match?(w) }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
-
end
-
result
-
end
-
-
1
def assert_not_deprecated(deprecator = nil, &block)
-
result, deprecations = collect_deprecations(deprecator, &block)
-
assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
-
result
-
end
-
-
1
def collect_deprecations(deprecator = nil)
-
deprecator ||= ActiveSupport::Deprecation
-
old_behavior = deprecator.behavior
-
deprecations = []
-
deprecator.behavior = Proc.new do |message, callstack|
-
deprecations << message
-
end
-
result = yield
-
[result, deprecations]
-
ensure
-
deprecator.behavior = old_behavior
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/concern"
-
-
1
module ActiveSupport
-
1
module Testing
-
# Adds simple access to sample files called file fixtures.
-
# File fixtures are normal files stored in
-
# <tt>ActiveSupport::TestCase.file_fixture_path</tt>.
-
#
-
# File fixtures are represented as +Pathname+ objects.
-
# This makes it easy to extract specific information:
-
#
-
# file_fixture("example.txt").read # get the file's content
-
# file_fixture("example.mp3").size # get the file size
-
1
module FileFixtures
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
2
class_attribute :file_fixture_path, instance_writer: false
-
end
-
-
# Returns a +Pathname+ to the fixture file named +fixture_name+.
-
#
-
# Raises +ArgumentError+ if +fixture_name+ can't be found.
-
1
def file_fixture(fixture_name)
-
path = Pathname.new(File.join(file_fixture_path, fixture_name))
-
-
if path.exist?
-
path
-
else
-
msg = "the directory '%s' does not contain a file named '%s'"
-
raise ArgumentError, msg % [file_fixture_path, fixture_name]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Isolation
-
1
require "thread"
-
-
1
def self.included(klass) #:nodoc:
-
klass.class_eval do
-
parallelize_me!
-
end
-
end
-
-
1
def self.forking_env?
-
1
!ENV["NO_FORK"] && Process.respond_to?(:fork)
-
end
-
-
1
def run
-
serialized = run_in_isolation do
-
super
-
end
-
-
Marshal.load(serialized)
-
end
-
-
1
module Forking
-
1
def run_in_isolation(&blk)
-
read, write = IO.pipe
-
read.binmode
-
write.binmode
-
-
pid = fork do
-
read.close
-
yield
-
begin
-
if error?
-
failures.map! { |e|
-
begin
-
Marshal.dump e
-
e
-
rescue TypeError
-
ex = Exception.new e.message
-
ex.set_backtrace e.backtrace
-
Minitest::UnexpectedError.new ex
-
end
-
}
-
end
-
test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
-
result = Marshal.dump(test_result)
-
end
-
-
write.puts [result].pack("m")
-
exit!
-
end
-
-
write.close
-
result = read.read
-
Process.wait2(pid)
-
result.unpack1("m")
-
end
-
end
-
-
1
module Subprocess
-
1
ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV)
-
-
# Crazy H4X to get this working in windows / jruby with
-
# no forking.
-
1
def run_in_isolation(&blk)
-
require "tempfile"
-
-
if ENV["ISOLATION_TEST"]
-
yield
-
test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
-
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
-
file.puts [Marshal.dump(test_result)].pack("m")
-
end
-
exit!
-
else
-
Tempfile.open("isolation") do |tmpfile|
-
env = {
-
"ISOLATION_TEST" => self.class.name,
-
"ISOLATION_OUTPUT" => tmpfile.path
-
}
-
-
test_opts = "-n#{self.class.name}##{name}"
-
-
load_path_args = []
-
$-I.each do |p|
-
load_path_args << "-I"
-
load_path_args << File.expand_path(p)
-
end
-
-
child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
-
-
begin
-
Process.wait(child.pid)
-
rescue Errno::ECHILD # The child process may exit before we wait
-
nil
-
end
-
-
return tmpfile.read.unpack1("m")
-
end
-
end
-
end
-
end
-
-
1
include forking_env? ? Forking : Subprocess
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "drb"
-
1
require "drb/unix" unless Gem.win_platform?
-
1
require "active_support/core_ext/module/attribute_accessors"
-
1
require "active_support/testing/parallelization/server"
-
1
require "active_support/testing/parallelization/worker"
-
-
1
module ActiveSupport
-
1
module Testing
-
1
class Parallelization # :nodoc:
-
1
@@after_fork_hooks = []
-
-
1
def self.after_fork_hook(&blk)
-
@@after_fork_hooks << blk
-
end
-
-
1
cattr_reader :after_fork_hooks
-
-
1
@@run_cleanup_hooks = []
-
-
1
def self.run_cleanup_hook(&blk)
-
@@run_cleanup_hooks << blk
-
end
-
-
1
cattr_reader :run_cleanup_hooks
-
-
1
def initialize(worker_count)
-
@worker_count = worker_count
-
@queue_server = Server.new
-
@worker_pool = []
-
@url = DRb.start_service("drbunix:", @queue_server).uri
-
end
-
-
1
def start
-
@worker_pool = @worker_count.times.map do |worker|
-
Worker.new(worker, @url).start
-
end
-
end
-
-
1
def <<(work)
-
@queue_server << work
-
end
-
-
1
def shutdown
-
@queue_server.shutdown
-
@worker_pool.each { |pid| Process.waitpid pid }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "drb"
-
1
require "drb/unix" unless Gem.win_platform?
-
-
1
module ActiveSupport
-
1
module Testing
-
1
class Parallelization # :nodoc:
-
1
class Server
-
1
include DRb::DRbUndumped
-
-
1
def initialize
-
@queue = Queue.new
-
@active_workers = Concurrent::Map.new
-
@in_flight = Concurrent::Map.new
-
end
-
-
1
def record(reporter, result)
-
raise DRb::DRbConnError if result.is_a?(DRb::DRbUnknown)
-
-
@in_flight.delete([result.klass, result.name])
-
-
reporter.synchronize do
-
reporter.record(result)
-
end
-
end
-
-
1
def <<(o)
-
o[2] = DRbObject.new(o[2]) if o
-
@queue << o
-
end
-
-
1
def pop
-
if test = @queue.pop
-
@in_flight[[test[0].to_s, test[1]]] = test
-
test
-
end
-
end
-
-
1
def start_worker(worker_id)
-
@active_workers[worker_id] = true
-
end
-
-
1
def stop_worker(worker_id)
-
@active_workers.delete(worker_id)
-
end
-
-
1
def active_workers?
-
@active_workers.size > 0
-
end
-
-
1
def shutdown
-
# Wait for initial queue to drain
-
while @queue.length != 0
-
sleep 0.1
-
end
-
-
@queue.close
-
-
# Wait until all workers have finished
-
while active_workers?
-
sleep 0.1
-
end
-
-
@in_flight.values.each do |(klass, name, reporter)|
-
result = Minitest::Result.from(klass.new(name))
-
error = RuntimeError.new("result not reported")
-
error.set_backtrace([""])
-
result.failures << Minitest::UnexpectedError.new(error)
-
reporter.synchronize do
-
reporter.record(result)
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
1
class Parallelization # :nodoc:
-
1
class Worker
-
1
def initialize(number, url)
-
@id = SecureRandom.uuid
-
@number = number
-
@url = url
-
@setup_exception = nil
-
end
-
-
1
def start
-
fork do
-
set_process_title("(starting)")
-
-
DRb.stop_service
-
-
@queue = DRbObject.new_with_uri(@url)
-
@queue.start_worker(@id)
-
-
begin
-
after_fork
-
rescue => @setup_exception; end
-
-
work_from_queue
-
ensure
-
set_process_title("(stopping)")
-
-
run_cleanup
-
@queue.stop_worker(@id)
-
end
-
end
-
-
1
def work_from_queue
-
while job = @queue.pop
-
perform_job(job)
-
end
-
end
-
-
1
def perform_job(job)
-
klass = job[0]
-
method = job[1]
-
reporter = job[2]
-
-
set_process_title("#{klass}##{method}")
-
-
result = klass.with_info_handler reporter do
-
Minitest.run_one_method(klass, method)
-
end
-
-
safe_record(reporter, result)
-
end
-
-
1
def safe_record(reporter, result)
-
add_setup_exception(result) if @setup_exception
-
-
begin
-
@queue.record(reporter, result)
-
rescue DRb::DRbConnError
-
result.failures.map! do |failure|
-
if failure.respond_to?(:error)
-
# minitest >5.14.0
-
error = DRb::DRbRemoteError.new(failure.error)
-
else
-
error = DRb::DRbRemoteError.new(failure.exception)
-
end
-
Minitest::UnexpectedError.new(error)
-
end
-
@queue.record(reporter, result)
-
end
-
-
set_process_title("(idle)")
-
end
-
-
1
def after_fork
-
Parallelization.after_fork_hooks.each do |cb|
-
cb.call(@number)
-
end
-
end
-
-
1
def run_cleanup
-
Parallelization.run_cleanup_hooks.each do |cb|
-
cb.call(@number)
-
end
-
end
-
-
1
private
-
1
def add_setup_exception(result)
-
result.failures.prepend Minitest::UnexpectedError.new(@setup_exception)
-
end
-
-
1
def set_process_title(status)
-
Process.setproctitle("Rails test worker #{@number} - #{status}")
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/callbacks"
-
-
1
module ActiveSupport
-
1
module Testing
-
# Adds support for +setup+ and +teardown+ callbacks.
-
# These callbacks serve as a replacement to overwriting the
-
# <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
-
#
-
# class ExampleTest < ActiveSupport::TestCase
-
# setup do
-
# # ...
-
# end
-
#
-
# teardown do
-
# # ...
-
# end
-
# end
-
1
module SetupAndTeardown
-
1
def self.prepended(klass)
-
1
klass.include ActiveSupport::Callbacks
-
1
klass.define_callbacks :setup, :teardown
-
1
klass.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Add a callback, which runs before <tt>TestCase#setup</tt>.
-
1
def setup(*args, &block)
-
5
set_callback(:setup, :before, *args, &block)
-
end
-
-
# Add a callback, which runs after <tt>TestCase#teardown</tt>.
-
1
def teardown(*args, &block)
-
2
set_callback(:teardown, :after, *args, &block)
-
end
-
end
-
-
1
def before_setup # :nodoc:
-
super
-
run_callbacks :setup
-
end
-
-
1
def after_teardown # :nodoc:
-
begin
-
run_callbacks :teardown
-
rescue => e
-
self.failures << Minitest::UnexpectedError.new(e)
-
end
-
-
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
# Logs a "PostsControllerTest: test name" heading before each test to
-
# make test.log easier to search and follow along with.
-
1
module TaggedLogging #:nodoc:
-
1
attr_writer :tagged_logger
-
-
1
def before_setup
-
if tagged_logger && tagged_logger.info?
-
heading = "#{self.class}: #{name}"
-
divider = "-" * heading.size
-
tagged_logger.info divider
-
tagged_logger.info heading
-
tagged_logger.info divider
-
end
-
super
-
end
-
-
1
private
-
1
def tagged_logger
-
@tagged_logger ||= (defined?(Rails.logger) && Rails.logger)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/redefine_method"
-
1
require "active_support/core_ext/time/calculations"
-
1
require "concurrent/map"
-
-
1
module ActiveSupport
-
1
module Testing
-
# Manages stubs for TimeHelpers
-
1
class SimpleStubs # :nodoc:
-
1
Stub = Struct.new(:object, :method_name, :original_method)
-
-
1
def initialize
-
@stubs = Concurrent::Map.new { |h, k| h[k] = {} }
-
end
-
-
# Stubs object.method_name with the given block
-
# If the method is already stubbed, remove that stub
-
# so that removing this stub will restore the original implementation.
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# target = Time.zone.local(2004, 11, 24, 1, 4, 44)
-
# simple_stubs.stub_object(Time, :now) { at(target.to_i) }
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
1
def stub_object(object, method_name, &block)
-
if stub = stubbing(object, method_name)
-
unstub_object(stub)
-
end
-
-
new_name = "__simple_stub__#{method_name}"
-
-
@stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name)
-
-
object.singleton_class.alias_method new_name, method_name
-
object.define_singleton_method(method_name, &block)
-
end
-
-
# Remove all object-method stubs held by this instance
-
1
def unstub_all!
-
@stubs.each_value do |object_stubs|
-
object_stubs.each_value do |stub|
-
unstub_object(stub)
-
end
-
end
-
@stubs.clear
-
end
-
-
# Returns the Stub for object#method_name
-
# (nil if it is not stubbed)
-
1
def stubbing(object, method_name)
-
@stubs[object.object_id][method_name]
-
end
-
-
# Returns true if any stubs are set, false if there are none
-
1
def stubbed?
-
!@stubs.empty?
-
end
-
-
1
private
-
# Restores the original object.method described by the Stub
-
1
def unstub_object(stub)
-
singleton_class = stub.object.singleton_class
-
singleton_class.silence_redefinition_of_method stub.method_name
-
singleton_class.alias_method stub.method_name, stub.original_method
-
singleton_class.undef_method stub.original_method
-
end
-
end
-
-
# Contains helpers that help you test passage of time.
-
1
module TimeHelpers
-
1
def after_teardown
-
travel_back
-
super
-
end
-
-
# Changes current time to the time in the future or in the past by a given time difference by
-
# stubbing +Time.now+, +Date.today+, and +DateTime.now+. The stubs are automatically removed
-
# at the end of the test.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel 1.day
-
# Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
-
# Date.current # => Sun, 10 Nov 2013
-
# DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel 1.day do
-
# User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
-
# end
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
1
def travel(duration, &block)
-
travel_to Time.now + duration, &block
-
end
-
-
# Changes current time to the given time by stubbing +Time.now+,
-
# +Date.today+, and +DateTime.now+ to return the time or date passed into this method.
-
# The stubs are automatically removed at the end of the test.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# Date.current # => Wed, 24 Nov 2004
-
# DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500
-
#
-
# Dates are taken as their timestamp at the beginning of the day in the
-
# application time zone. <tt>Time.current</tt> returns said timestamp,
-
# and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
-
# <tt>Date.current</tt> returns a date equal to the argument, and
-
# <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
-
# be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
-
# or <tt>Date.today</tt>, in order to honor the application time zone
-
# please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
-
#
-
# Note that the usec for the time passed will be set to 0 to prevent rounding
-
# errors with external services, like MySQL (which will round instead of floor,
-
# leading to off-by-one-second errors).
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44) do
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# end
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
1
def travel_to(date_or_time)
-
if block_given? && simple_stubs.stubbing(Time, :now)
-
travel_to_nested_block_call = <<~MSG
-
-
Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.
-
-
Instead of:
-
-
travel_to 2.days.from_now do
-
# 2 days from today
-
travel_to 3.days.from_now do
-
# 5 days from today
-
end
-
end
-
-
preferred way to achieve above is:
-
-
travel 2.days do
-
# 2 days from today
-
end
-
-
travel 5.days do
-
# 5 days from today
-
end
-
-
MSG
-
raise travel_to_nested_block_call
-
end
-
-
if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
-
now = date_or_time.midnight.to_time
-
else
-
now = date_or_time.to_time.change(usec: 0)
-
end
-
-
simple_stubs.stub_object(Time, :now) { at(now.to_i) }
-
simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) }
-
simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) }
-
-
if block_given?
-
begin
-
yield
-
ensure
-
travel_back
-
end
-
end
-
end
-
-
# Returns the current time back to its original state, by removing the stubs added by
-
# +travel+, +travel_to+, and +freeze_time+.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
#
-
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
#
-
# travel_back
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
#
-
# This method also accepts a block, which brings the stubs back at the end of the block:
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
#
-
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
#
-
# travel_back do
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# end
-
#
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
1
def travel_back
-
stubbed_time = Time.current if block_given? && simple_stubs.stubbed?
-
-
simple_stubs.unstub_all!
-
yield if block_given?
-
ensure
-
travel_to stubbed_time if stubbed_time
-
end
-
1
alias_method :unfreeze_time, :travel_back
-
-
# Calls +travel_to+ with +Time.now+.
-
#
-
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
# freeze_time
-
# sleep(1)
-
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
# freeze_time do
-
# sleep(1)
-
# User.create.created_at # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
# end
-
# Time.current # => Sun, 09 Jul 2017 15:34:50 EST -05:00
-
1
def freeze_time(&block)
-
travel_to Time.now, &block
-
end
-
-
1
private
-
1
def simple_stubs
-
@simple_stubs ||= SimpleStubs.new
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Erubi
-
1
VERSION = '1.10.0'
-
1
RANGE_ALL = 0..-1
-
-
skipped
# :nocov:
-
skipped
if RUBY_VERSION >= '1.9'
-
skipped
RANGE_FIRST = 0
-
skipped
RANGE_LAST = -1
-
skipped
else
-
skipped
RANGE_FIRST = 0..0
-
skipped
RANGE_LAST = -1..-1
-
skipped
end
-
skipped
-
skipped
TEXT_END = RUBY_VERSION >= '2.1' ? "'.freeze;" : "';"
-
skipped
MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match
-
skipped
# :nocov:
-
-
begin
-
1
require 'cgi/escape'
-
skipped
# :nocov:
-
skipped
unless CGI.respond_to?(:escapeHTML) # work around for JRuby 9.1
-
skipped
CGI = Object.new
-
skipped
CGI.extend(defined?(::CGI::Escape) ? ::CGI::Escape : ::CGI::Util)
-
skipped
end
-
skipped
# :nocov:
-
# Escape characters with their HTML/XML equivalents.
-
1
def self.h(value)
-
CGI.escapeHTML(value.to_s)
-
end
-
rescue LoadError
-
skipped
# :nocov:
-
skipped
ESCAPE_TABLE = {'&' => '&'.freeze, '<' => '<'.freeze, '>' => '>'.freeze, '"' => '"'.freeze, "'" => '''.freeze}.freeze
-
skipped
if RUBY_VERSION >= '1.9'
-
skipped
def self.h(value)
-
skipped
value.to_s.gsub(/[&<>"']/, ESCAPE_TABLE)
-
skipped
end
-
skipped
else
-
skipped
def self.h(value)
-
skipped
value.to_s.gsub(/[&<>"']/){|s| ESCAPE_TABLE[s]}
-
skipped
end
-
skipped
end
-
skipped
# :nocov:
-
end
-
-
1
class Engine
-
# The frozen ruby source code generated from the template, which can be evaled.
-
1
attr_reader :src
-
-
# The filename of the template, if one was given.
-
1
attr_reader :filename
-
-
# The variable name used for the buffer variable.
-
1
attr_reader :bufvar
-
-
# Initialize a new Erubi::Engine. Options:
-
# +:bufval+ :: The value to use for the buffer variable, as a string (default <tt>'::String.new'</tt>).
-
# +:bufvar+ :: The variable name to use for the buffer variable, as a string.
-
# +:ensure+ :: Wrap the template in a begin/ensure block restoring the previous value of bufvar.
-
# +:escapefunc+ :: The function to use for escaping, as a string (default: <tt>'::Erubi.h'</tt>).
-
# +:escape+ :: Whether to make <tt><%=</tt> escape by default, and <tt><%==</tt> not escape by default.
-
# +:escape_html+ :: Same as +:escape+, with lower priority.
-
# +:filename+ :: The filename for the template.
-
# +:freeze+ :: Whether to enable frozen string literals in the resulting source code.
-
# +:literal_prefix+ :: The prefix to output when using escaped tag delimiters (default <tt>'<%'</tt>).
-
# +:literal_postfix+ :: The postfix to output when using escaped tag delimiters (default <tt>'%>'</tt>).
-
# +:outvar+ :: Same as +:bufvar+, with lower priority.
-
# +:postamble+ :: The postamble for the template, by default returns the resulting source code.
-
# +:preamble+ :: The preamble for the template, by default initializes the buffer variable.
-
# +:regexp+ :: The regexp to use for scanning.
-
# +:src+ :: The initial value to use for the source code, an empty string by default.
-
# +:trim+ :: Whether to trim leading and trailing whitespace, true by default.
-
1
def initialize(input, properties={})
-
@escape = escape = properties.fetch(:escape){properties.fetch(:escape_html, false)}
-
trim = properties[:trim] != false
-
@filename = properties[:filename]
-
@bufvar = bufvar = properties[:bufvar] || properties[:outvar] || "_buf"
-
bufval = properties[:bufval] || '::String.new'
-
regexp = properties[:regexp] || /<%(={1,2}|-|\#|%)?(.*?)([-=])?%>([ \t]*\r?\n)?/m
-
literal_prefix = properties[:literal_prefix] || '<%'
-
literal_postfix = properties[:literal_postfix] || '%>'
-
preamble = properties[:preamble] || "#{bufvar} = #{bufval};"
-
postamble = properties[:postamble] || "#{bufvar}.to_s\n"
-
-
@src = src = properties[:src] || String.new
-
src << "# frozen_string_literal: true\n" if properties[:freeze]
-
src << "begin; __original_outvar = #{bufvar} if defined?(#{bufvar}); " if properties[:ensure]
-
-
unless @escapefunc = properties[:escapefunc]
-
if escape
-
@escapefunc = '__erubi.h'
-
src << "__erubi = ::Erubi;"
-
else
-
@escapefunc = '::Erubi.h'
-
end
-
end
-
-
src << preamble
-
-
pos = 0
-
is_bol = true
-
input.scan(regexp) do |indicator, code, tailch, rspace|
-
match = Regexp.last_match
-
len = match.begin(0) - pos
-
text = input[pos, len]
-
pos = match.end(0)
-
ch = indicator ? indicator[RANGE_FIRST] : nil
-
-
lspace = nil
-
-
unless ch == '='
-
if text.empty?
-
lspace = "" if is_bol
-
elsif text[RANGE_LAST] == "\n"
-
lspace = ""
-
else
-
rindex = text.rindex("\n")
-
if rindex
-
range = rindex+1..-1
-
s = text[range]
-
if /\A[ \t]*\z/.send(MATCH_METHOD, s)
-
lspace = s
-
text[range] = ''
-
end
-
else
-
if is_bol && /\A[ \t]*\z/.send(MATCH_METHOD, text)
-
lspace = text
-
text = ''
-
end
-
end
-
end
-
end
-
-
is_bol = rspace
-
add_text(text)
-
case ch
-
when '='
-
rspace = nil if tailch && !tailch.empty?
-
add_expression(indicator, code)
-
add_text(rspace) if rspace
-
when nil, '-'
-
if trim && lspace && rspace
-
add_code("#{lspace}#{code}#{rspace}")
-
else
-
add_text(lspace) if lspace
-
add_code(code)
-
add_text(rspace) if rspace
-
end
-
when '#'
-
n = code.count("\n") + (rspace ? 1 : 0)
-
if trim && lspace && rspace
-
add_code("\n" * n)
-
else
-
add_text(lspace) if lspace
-
add_code("\n" * n)
-
add_text(rspace) if rspace
-
end
-
when '%'
-
add_text("#{lspace}#{literal_prefix}#{code}#{tailch}#{literal_postfix}#{rspace}")
-
else
-
handle(indicator, code, tailch, rspace, lspace)
-
end
-
end
-
rest = pos == 0 ? input : input[pos..-1]
-
add_text(rest)
-
-
src << "\n" unless src[RANGE_LAST] == "\n"
-
add_postamble(postamble)
-
src << "; ensure\n " << bufvar << " = __original_outvar\nend\n" if properties[:ensure]
-
src.freeze
-
freeze
-
end
-
-
1
private
-
-
# Add raw text to the template. Modifies argument if argument is mutable as a memory optimization.
-
# Must be called with a string, cannot be called with nil (Rails's subclass depends on it).
-
1
def add_text(text)
-
return if text.empty?
-
-
if text.frozen?
-
text = text.gsub(/['\\]/, '\\\\\&')
-
else
-
text.gsub!(/['\\]/, '\\\\\&')
-
end
-
@src << " " << @bufvar << " << '" << text << TEXT_END
-
end
-
-
# Add ruby code to the template
-
1
def add_code(code)
-
@src << code
-
@src << ';' unless code[RANGE_LAST] == "\n"
-
end
-
-
# Add the given ruby expression result to the template,
-
# escaping it based on the indicator given and escape flag.
-
1
def add_expression(indicator, code)
-
if ((indicator == '=') ^ @escape)
-
add_expression_result(code)
-
else
-
add_expression_result_escaped(code)
-
end
-
end
-
-
# Add the result of Ruby expression to the template
-
1
def add_expression_result(code)
-
@src << ' ' << @bufvar << ' << (' << code << ').to_s;'
-
end
-
-
# Add the escaped result of Ruby expression to the template
-
1
def add_expression_result_escaped(code)
-
@src << ' ' << @bufvar << ' << ' << @escapefunc << '((' << code << '));'
-
end
-
-
# Add the given postamble to the src. Can be overridden in subclasses
-
# to make additional changes to src that depend on the current state.
-
1
def add_postamble(postamble)
-
src << postamble
-
end
-
-
# Raise an exception, as the base engine class does not support handling other indicators.
-
1
def handle(indicator, code, tailch, rspace, lspace)
-
raise ArgumentError, "Invalid indicator: #{indicator}"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
module CheckDeliveryParams #:nodoc:
-
1
class << self
-
1
def check(mail)
-
[ check_from(mail.smtp_envelope_from),
-
check_to(mail.smtp_envelope_to),
-
check_message(mail) ]
-
end
-
-
1
def check_from(addr)
-
if Utilities.blank?(addr)
-
raise ArgumentError, "SMTP From address may not be blank: #{addr.inspect}"
-
end
-
-
check_addr 'From', addr
-
end
-
-
1
def check_to(addrs)
-
if Utilities.blank?(addrs)
-
raise ArgumentError, "SMTP To address may not be blank: #{addrs.inspect}"
-
end
-
-
Array(addrs).map do |addr|
-
check_addr 'To', addr
-
end
-
end
-
-
1
def check_addr(addr_name, addr)
-
validate_smtp_addr addr do |error_message|
-
raise ArgumentError, "SMTP #{addr_name} address #{error_message}: #{addr.inspect}"
-
end
-
end
-
-
1
def validate_smtp_addr(addr)
-
if addr
-
if addr.bytesize > 2048
-
yield 'may not exceed 2kB'
-
end
-
-
if /[\r\n]/ =~ addr
-
yield 'may not contain CR or LF line breaks'
-
end
-
end
-
-
addr
-
end
-
-
1
def check_message(message)
-
message = message.encoded if message.respond_to?(:encoded)
-
-
if Utilities.blank?(message)
-
raise ArgumentError, 'An encoded message is required to send an email'
-
end
-
-
message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# FileDelivery class delivers emails into multiple files based on the destination
-
# address. Each file is appended to if it already exists.
-
#
-
# So if you have an email going to fred@test, bob@test, joe@anothertest, and you
-
# set your location path to /path/to/mails then FileDelivery will create the directory
-
# if it does not exist, and put one copy of the email in three files, called
-
# by their message id
-
#
-
# Make sure the path you specify with :location is writable by the Ruby process
-
# running Mail.
-
1
class FileDelivery
-
1
if RUBY_VERSION >= '1.9.1'
-
1
require 'fileutils'
-
else
-
require 'ftools'
-
end
-
-
1
attr_accessor :settings
-
-
1
def initialize(values)
-
self.settings = { :location => './mails' }.merge!(values)
-
end
-
-
1
def deliver!(mail)
-
Mail::CheckDeliveryParams.check(mail)
-
-
if ::File.respond_to?(:makedirs)
-
::File.makedirs settings[:location]
-
else
-
::FileUtils.mkdir_p settings[:location]
-
end
-
-
mail.destinations.uniq.each do |to|
-
::File.open(::File.join(settings[:location], File.basename(to.to_s)), 'a') { |f| "#{f.write(mail.encoded)}\r\n\r\n" }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# A delivery method implementation which sends via sendmail.
-
#
-
# To use this, first find out where the sendmail binary is on your computer,
-
# if you are on a mac or unix box, it is usually in /usr/sbin/sendmail, this will
-
# be your sendmail location.
-
#
-
# Mail.defaults do
-
# delivery_method :sendmail
-
# end
-
#
-
# Or if your sendmail binary is not at '/usr/sbin/sendmail'
-
#
-
# Mail.defaults do
-
# delivery_method :sendmail, :location => '/absolute/path/to/your/sendmail'
-
# end
-
#
-
# Then just deliver the email as normal:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# Or by calling deliver on a Mail message
-
#
-
# mail = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# mail.deliver!
-
1
class Sendmail
-
DEFAULTS = {
-
1
:location => '/usr/sbin/sendmail',
-
:arguments => '-i'
-
}
-
-
1
attr_accessor :settings
-
-
1
def initialize(values)
-
self.settings = self.class::DEFAULTS.merge(values)
-
end
-
-
1
def deliver!(mail)
-
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
-
-
from = "-f #{self.class.shellquote(smtp_from)}" if smtp_from
-
to = smtp_to.map { |_to| self.class.shellquote(_to) }.join(' ')
-
-
arguments = "#{settings[:arguments]} #{from} --"
-
self.class.call(settings[:location], arguments, to, message)
-
end
-
-
1
def self.call(path, arguments, destinations, encoded_message)
-
popen "#{path} #{arguments} #{destinations}" do |io|
-
io.puts ::Mail::Utilities.binary_unsafe_to_lf(encoded_message)
-
io.flush
-
end
-
end
-
-
1
if RUBY_VERSION < '1.9.0'
-
def self.popen(command, &block)
-
IO.popen "#{command} 2>&1", 'w+', &block
-
end
-
else
-
1
def self.popen(command, &block)
-
IO.popen command, 'w+', :err => :out, &block
-
end
-
end
-
-
# The following is an adaptation of ruby 1.9.2's shellwords.rb file,
-
# with the following modifications:
-
#
-
# - Wraps in double quotes
-
# - Allows '+' to accept email addresses with them
-
# - Allows '~' as it is not unescaped in double quotes
-
1
def self.shellquote(address)
-
# Process as a single byte sequence because not all shell
-
# implementations are multibyte aware.
-
#
-
# A LF cannot be escaped with a backslash because a backslash + LF
-
# combo is regarded as line continuation and simply ignored. Strip it.
-
escaped = address.gsub(/([^A-Za-z0-9_\s\+\-.,:\/@~])/n, "\\\\\\1").gsub("\n", '')
-
%("#{escaped}")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# == Sending Email with SMTP
-
#
-
# Mail allows you to send emails using SMTP. This is done by wrapping Net::SMTP in
-
# an easy to use manner.
-
#
-
# === Sending via SMTP server on Localhost
-
#
-
# Sending locally (to a postfix or sendmail server running on localhost) requires
-
# no special setup. Just to Mail.deliver &block or message.deliver! and it will
-
# be sent in this method.
-
#
-
# === Sending via MobileMe
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "smtp.me.com",
-
# :port => 587,
-
# :domain => 'your.host.name',
-
# :user_name => '<username>',
-
# :password => '<password>',
-
# :authentication => 'plain',
-
# :enable_starttls_auto => true }
-
# end
-
#
-
# === Sending via GMail
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "smtp.gmail.com",
-
# :port => 587,
-
# :domain => 'your.host.name',
-
# :user_name => '<username>',
-
# :password => '<password>',
-
# :authentication => 'plain',
-
# :enable_starttls_auto => true }
-
# end
-
#
-
# === Certificate verification
-
#
-
# When using TLS, some mail servers provide certificates that are self-signed
-
# or whose names do not exactly match the hostname given in the address.
-
# OpenSSL will reject these by default. The best remedy is to use the correct
-
# hostname or update the certificate authorities trusted by your ruby. If
-
# that isn't possible, you can control this behavior with
-
# an :openssl_verify_mode setting. Its value may be either an OpenSSL
-
# verify mode constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER),
-
# or a string containing the name of an OpenSSL verify mode (none, peer).
-
#
-
# === Others
-
#
-
# Feel free to send me other examples that were tricky
-
#
-
# === Delivering the email
-
#
-
# Once you have the settings right, sending the email is done by:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# Or by calling deliver on a Mail message
-
#
-
# mail = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# mail.deliver!
-
1
class SMTP
-
1
attr_accessor :settings
-
-
DEFAULTS = {
-
1
:address => 'localhost',
-
:port => 25,
-
:domain => 'localhost.localdomain',
-
:user_name => nil,
-
:password => nil,
-
:authentication => nil,
-
:enable_starttls => nil,
-
:enable_starttls_auto => true,
-
:openssl_verify_mode => nil,
-
:ssl => nil,
-
:tls => nil,
-
:open_timeout => nil,
-
:read_timeout => nil
-
}
-
-
1
def initialize(values)
-
self.settings = DEFAULTS.merge(values)
-
end
-
-
1
def deliver!(mail)
-
response = start_smtp_session do |smtp|
-
Mail::SMTPConnection.new(:connection => smtp, :return_response => true).deliver!(mail)
-
end
-
-
settings[:return_response] ? response : self
-
end
-
-
1
private
-
1
def start_smtp_session(&block)
-
build_smtp_session.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication], &block)
-
end
-
-
1
def build_smtp_session
-
Net::SMTP.new(settings[:address], settings[:port]).tap do |smtp|
-
if settings[:tls] || settings[:ssl]
-
if smtp.respond_to?(:enable_tls)
-
smtp.enable_tls(ssl_context)
-
end
-
elsif settings[:enable_starttls]
-
if smtp.respond_to?(:enable_starttls)
-
smtp.enable_starttls(ssl_context)
-
end
-
elsif settings[:enable_starttls_auto]
-
if smtp.respond_to?(:enable_starttls_auto)
-
smtp.enable_starttls_auto(ssl_context)
-
end
-
end
-
-
smtp.open_timeout = settings[:open_timeout] if settings[:open_timeout]
-
smtp.read_timeout = settings[:read_timeout] if settings[:read_timeout]
-
end
-
end
-
-
# Allow SSL context to be configured via settings, for Ruby >= 1.9
-
# Just returns openssl verify mode for Ruby 1.8.x
-
1
def ssl_context
-
openssl_verify_mode = settings[:openssl_verify_mode]
-
-
if openssl_verify_mode.kind_of?(String)
-
openssl_verify_mode = OpenSSL::SSL.const_get("VERIFY_#{openssl_verify_mode.upcase}")
-
end
-
-
context = Net::SMTP.default_ssl_context
-
context.verify_mode = openssl_verify_mode if openssl_verify_mode
-
context.ca_path = settings[:ca_path] if settings[:ca_path]
-
context.ca_file = settings[:ca_file] if settings[:ca_file]
-
context
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# The TestMailer is a bare bones mailer that does nothing. It is useful
-
# when you are testing.
-
#
-
# It also provides a template of the minimum methods you require to implement
-
# if you want to make a custom mailer for Mail
-
1
class TestMailer
-
# Provides a store of all the emails sent with the TestMailer so you can check them.
-
1
def self.deliveries
-
2
@@deliveries ||= []
-
end
-
-
# Allows you to over write the default deliveries store from an array to some
-
# other object. If you just want to clear the store,
-
# call TestMailer.deliveries.clear.
-
#
-
# If you place another object here, please make sure it responds to:
-
#
-
# * << (message)
-
# * clear
-
# * length
-
# * size
-
# * and other common Array methods
-
1
def self.deliveries=(val)
-
@@deliveries = val
-
end
-
-
1
attr_accessor :settings
-
-
1
def initialize(values)
-
@settings = values.dup
-
end
-
-
1
def deliver!(mail)
-
Mail::CheckDeliveryParams.check(mail)
-
Mail::TestMailer.deliveries << mail
-
end
-
end
-
end
-
1
require "optparse"
-
1
require "thread"
-
1
require "mutex_m"
-
1
require "minitest/parallel"
-
1
require "stringio"
-
-
##
-
# :include: README.rdoc
-
-
1
module Minitest
-
1
VERSION = "5.14.4" # :nodoc:
-
1
ENCS = "".respond_to? :encoding # :nodoc:
-
-
1
@@installed_at_exit ||= false
-
1
@@after_run = []
-
1
@extensions = []
-
-
2
mc = (class << self; self; end)
-
-
##
-
# Parallel test executor
-
-
1
mc.send :attr_accessor, :parallel_executor
-
-
1
warn "DEPRECATED: use MT_CPU instead of N for parallel test runs" if ENV["N"]
-
1
n_threads = (ENV["MT_CPU"] || ENV["N"] || 2).to_i
-
1
self.parallel_executor = Parallel::Executor.new n_threads
-
-
##
-
# Filter object for backtraces.
-
-
1
mc.send :attr_accessor, :backtrace_filter
-
-
##
-
# Reporter object to be used for all runs.
-
#
-
# NOTE: This accessor is only available during setup, not during runs.
-
-
1
mc.send :attr_accessor, :reporter
-
-
##
-
# Names of known extension plugins.
-
-
1
mc.send :attr_accessor, :extensions
-
-
##
-
# The signal to use for dumping information to STDERR. Defaults to "INFO".
-
-
1
mc.send :attr_accessor, :info_signal
-
1
self.info_signal = "INFO"
-
-
##
-
# Registers Minitest to run at process exit
-
-
1
def self.autorun
-
at_exit {
-
next if $! and not ($!.kind_of? SystemExit and $!.success?)
-
-
exit_code = nil
-
-
pid = Process.pid
-
at_exit {
-
next if Process.pid != pid
-
@@after_run.reverse_each(&:call)
-
exit exit_code || false
-
}
-
-
exit_code = Minitest.run ARGV
-
} unless @@installed_at_exit
-
@@installed_at_exit = true
-
end
-
-
##
-
# A simple hook allowing you to run a block of code after everything
-
# is done running. Eg:
-
#
-
# Minitest.after_run { p $debugging_info }
-
-
1
def self.after_run &block
-
@@after_run << block
-
end
-
-
1
def self.init_plugins options # :nodoc:
-
self.extensions.each do |name|
-
msg = "plugin_#{name}_init"
-
send msg, options if self.respond_to? msg
-
end
-
end
-
-
1
def self.load_plugins # :nodoc:
-
return unless self.extensions.empty?
-
-
seen = {}
-
-
require "rubygems" unless defined? Gem
-
-
Gem.find_files("minitest/*_plugin.rb").each do |plugin_path|
-
name = File.basename plugin_path, "_plugin.rb"
-
-
next if seen[name]
-
seen[name] = true
-
-
require plugin_path
-
self.extensions << name
-
end
-
end
-
-
##
-
# This is the top-level run method. Everything starts from here. It
-
# tells each Runnable sub-class to run, and each of those are
-
# responsible for doing whatever they do.
-
#
-
# The overall structure of a run looks like this:
-
#
-
# Minitest.autorun
-
# Minitest.run(args)
-
# Minitest.__run(reporter, options)
-
# Runnable.runnables.each
-
# runnable.run(reporter, options)
-
# self.runnable_methods.each
-
# self.run_one_method(self, runnable_method, reporter)
-
# Minitest.run_one_method(klass, runnable_method)
-
# klass.new(runnable_method).run
-
-
1
def self.run args = []
-
self.load_plugins unless args.delete("--no-plugins") || ENV["MT_NO_PLUGINS"]
-
-
options = process_args args
-
-
reporter = CompositeReporter.new
-
reporter << SummaryReporter.new(options[:io], options)
-
reporter << ProgressReporter.new(options[:io], options)
-
-
self.reporter = reporter # this makes it available to plugins
-
self.init_plugins options
-
self.reporter = nil # runnables shouldn't depend on the reporter, ever
-
-
self.parallel_executor.start if parallel_executor.respond_to?(:start)
-
reporter.start
-
begin
-
__run reporter, options
-
rescue Interrupt
-
warn "Interrupted. Exiting..."
-
end
-
self.parallel_executor.shutdown
-
reporter.report
-
-
reporter.passed?
-
end
-
-
##
-
# Internal run method. Responsible for telling all Runnable
-
# sub-classes to run.
-
-
1
def self.__run reporter, options
-
suites = Runnable.runnables.reject { |s| s.runnable_methods.empty? }.shuffle
-
parallel, serial = suites.partition { |s| s.test_order == :parallel }
-
-
# If we run the parallel tests before the serial tests, the parallel tests
-
# could run in parallel with the serial tests. This would be bad because
-
# the serial tests won't lock around Reporter#record. Run the serial tests
-
# first, so that after they complete, the parallel tests will lock when
-
# recording results.
-
serial.map { |suite| suite.run reporter, options } +
-
parallel.map { |suite| suite.run reporter, options }
-
end
-
-
1
def self.process_args args = [] # :nodoc:
-
options = {
-
:io => $stdout,
-
}
-
orig_args = args.dup
-
-
OptionParser.new do |opts|
-
opts.banner = "minitest options:"
-
opts.version = Minitest::VERSION
-
-
opts.on "-h", "--help", "Display this help." do
-
puts opts
-
exit
-
end
-
-
opts.on "--no-plugins", "Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS)."
-
-
desc = "Sets random seed. Also via env. Eg: SEED=n rake"
-
opts.on "-s", "--seed SEED", Integer, desc do |m|
-
options[:seed] = m.to_i
-
end
-
-
opts.on "-v", "--verbose", "Verbose. Show progress processing files." do
-
options[:verbose] = true
-
end
-
-
opts.on "-n", "--name PATTERN", "Filter run on /regexp/ or string." do |a|
-
options[:filter] = a
-
end
-
-
opts.on "-e", "--exclude PATTERN", "Exclude /regexp/ or string from run." do |a|
-
options[:exclude] = a
-
end
-
-
unless extensions.empty?
-
opts.separator ""
-
opts.separator "Known extensions: #{extensions.join(", ")}"
-
-
extensions.each do |meth|
-
msg = "plugin_#{meth}_options"
-
send msg, opts, options if self.respond_to?(msg)
-
end
-
end
-
-
begin
-
opts.parse! args
-
rescue OptionParser::InvalidOption => e
-
puts
-
puts e
-
puts
-
puts opts
-
exit 1
-
end
-
-
orig_args -= args
-
end
-
-
unless options[:seed] then
-
srand
-
options[:seed] = (ENV["SEED"] || srand).to_i % 0xFFFF
-
orig_args << "--seed" << options[:seed].to_s
-
end
-
-
srand options[:seed]
-
-
options[:args] = orig_args.map { |s|
-
s =~ /[\s|&<>$()]/ ? s.inspect : s
-
}.join " "
-
-
options
-
end
-
-
1
def self.filter_backtrace bt # :nodoc:
-
result = backtrace_filter.filter bt
-
result = bt.dup if result.empty?
-
result
-
end
-
-
##
-
# Represents anything "runnable", like Test, Spec, Benchmark, or
-
# whatever you can dream up.
-
#
-
# Subclasses of this are automatically registered and available in
-
# Runnable.runnables.
-
-
1
class Runnable
-
##
-
# Number of assertions executed in this run.
-
-
1
attr_accessor :assertions
-
-
##
-
# An assertion raised during the run, if any.
-
-
1
attr_accessor :failures
-
-
##
-
# The time it took to run.
-
-
1
attr_accessor :time
-
-
1
def time_it # :nodoc:
-
t0 = Minitest.clock_time
-
-
yield
-
ensure
-
self.time = Minitest.clock_time - t0
-
end
-
-
##
-
# Name of the run.
-
-
1
def name
-
@NAME
-
end
-
-
##
-
# Set the name of the run.
-
-
1
def name= o
-
@NAME = o
-
end
-
-
##
-
# Returns all instance methods matching the pattern +re+.
-
-
1
def self.methods_matching re
-
public_instance_methods(true).grep(re).map(&:to_s)
-
end
-
-
1
def self.reset # :nodoc:
-
1
@@runnables = []
-
end
-
-
1
reset
-
-
##
-
# Responsible for running all runnable methods in a given class,
-
# each in its own instance. Each instance is passed to the
-
# reporter to record.
-
-
1
def self.run reporter, options = {}
-
filter = options[:filter] || "/./"
-
filter = Regexp.new $1 if filter.is_a?(String) && filter =~ %r%/(.*)/%
-
-
filtered_methods = self.runnable_methods.find_all { |m|
-
filter === m || filter === "#{self}##{m}"
-
}
-
-
exclude = options[:exclude]
-
exclude = Regexp.new $1 if exclude =~ %r%/(.*)/%
-
-
filtered_methods.delete_if { |m|
-
exclude === m || exclude === "#{self}##{m}"
-
}
-
-
return if filtered_methods.empty?
-
-
with_info_handler reporter do
-
filtered_methods.each do |method_name|
-
run_one_method self, method_name, reporter
-
end
-
end
-
end
-
-
##
-
# Runs a single method and has the reporter record the result.
-
# This was considered internal API but is factored out of run so
-
# that subclasses can specialize the running of an individual
-
# test. See Minitest::ParallelTest::ClassMethods for an example.
-
-
1
def self.run_one_method klass, method_name, reporter
-
reporter.prerecord klass, method_name
-
reporter.record Minitest.run_one_method(klass, method_name)
-
end
-
-
1
def self.with_info_handler reporter, &block # :nodoc:
-
handler = lambda do
-
unless reporter.passed? then
-
warn "Current results:"
-
warn ""
-
warn reporter.reporters.first
-
warn ""
-
end
-
end
-
-
on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
SIGNALS = Signal.list # :nodoc:
-
-
1
def self.on_signal name, action # :nodoc:
-
supported = SIGNALS[name]
-
-
old_trap = trap name do
-
old_trap.call if old_trap.respond_to? :call
-
action.call
-
end if supported
-
-
yield
-
ensure
-
trap name, old_trap if supported
-
end
-
-
##
-
# Each subclass of Runnable is responsible for overriding this
-
# method to return all runnable methods. See #methods_matching.
-
-
1
def self.runnable_methods
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns all subclasses of Runnable.
-
-
1
def self.runnables
-
9
@@runnables
-
end
-
-
1
@@marshal_dump_warned = false
-
-
1
def marshal_dump # :nodoc:
-
unless @@marshal_dump_warned then
-
warn ["Minitest::Runnable#marshal_dump is deprecated.",
-
"You might be violating internals. From", caller.first].join " "
-
@@marshal_dump_warned = true
-
end
-
-
[self.name, self.failures, self.assertions, self.time]
-
end
-
-
1
def marshal_load ary # :nodoc:
-
self.name, self.failures, self.assertions, self.time = ary
-
end
-
-
1
def failure # :nodoc:
-
self.failures.first
-
end
-
-
1
def initialize name # :nodoc:
-
self.name = name
-
self.failures = []
-
self.assertions = 0
-
end
-
-
##
-
# Runs a single method. Needs to return self.
-
-
1
def run
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns a single character string to print based on the result
-
# of the run. One of <tt>"."</tt>, <tt>"F"</tt>,
-
# <tt>"E"</tt> or <tt>"S"</tt>.
-
-
1
def result_code
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Was this run skipped? See #passed? for more information.
-
-
1
def skipped?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
end
-
-
##
-
# Shared code for anything that can get passed to a Reporter. See
-
# Minitest::Test & Minitest::Result.
-
-
1
module Reportable
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
not self.failure
-
end
-
-
##
-
# The location identifier of this test. Depends on a method
-
# existing called class_name.
-
-
1
def location
-
loc = " [#{self.failure.location}]" unless passed? or error?
-
"#{self.class_name}##{self.name}#{loc}"
-
end
-
-
1
def class_name # :nodoc:
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns ".", "F", or "E" based on the result of the run.
-
-
1
def result_code
-
self.failure and self.failure.result_code or "."
-
end
-
-
##
-
# Was this run skipped?
-
-
1
def skipped?
-
self.failure and Skip === self.failure
-
end
-
-
##
-
# Did this run error?
-
-
1
def error?
-
self.failures.any? { |f| UnexpectedError === f }
-
end
-
end
-
-
##
-
# This represents a test result in a clean way that can be
-
# marshalled over a wire. Tests can do anything they want to the
-
# test instance and can create conditions that cause Marshal.dump to
-
# blow up. By using Result.from(a_test) you can be reasonably sure
-
# that the test result can be marshalled.
-
-
1
class Result < Runnable
-
1
include Minitest::Reportable
-
-
1
undef_method :marshal_dump
-
1
undef_method :marshal_load
-
-
##
-
# The class name of the test result.
-
-
1
attr_accessor :klass
-
-
##
-
# The location of the test method.
-
-
1
attr_accessor :source_location
-
-
##
-
# Create a new test result from a Runnable instance.
-
-
1
def self.from runnable
-
o = runnable
-
-
r = self.new o.name
-
r.klass = o.class.name
-
r.assertions = o.assertions
-
r.failures = o.failures.dup
-
r.time = o.time
-
-
r.source_location = o.method(o.name).source_location rescue ["unknown", -1]
-
-
r
-
end
-
-
1
def class_name # :nodoc:
-
self.klass # for Minitest::Reportable
-
end
-
-
1
def to_s # :nodoc:
-
return location if passed? and not skipped?
-
-
failures.map { |failure|
-
"#{failure.result_label}:\n#{self.location}:\n#{failure.message}\n"
-
}.join "\n"
-
end
-
end
-
-
##
-
# Defines the API for Reporters. Subclass this and override whatever
-
# you want. Go nuts.
-
-
1
class AbstractReporter
-
1
include Mutex_m
-
-
##
-
# Starts reporting on the run.
-
-
1
def start
-
end
-
-
##
-
# About to start running a test. This allows a reporter to show
-
# that it is starting or that we are in the middle of a test run.
-
-
1
def prerecord klass, name
-
end
-
-
##
-
# Output and record the result of the test. Call
-
# {result#result_code}[rdoc-ref:Runnable#result_code] to get the
-
# result character string. Stores the result of the run if the run
-
# did not pass.
-
-
1
def record result
-
end
-
-
##
-
# Outputs the summary of the run.
-
-
1
def report
-
end
-
-
##
-
# Did this run pass?
-
-
1
def passed?
-
true
-
end
-
end
-
-
1
class Reporter < AbstractReporter # :nodoc:
-
##
-
# The IO used to report.
-
-
1
attr_accessor :io
-
-
##
-
# Command-line options for this run.
-
-
1
attr_accessor :options
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
super()
-
self.io = io
-
self.options = options
-
end
-
end
-
-
##
-
# A very simple reporter that prints the "dots" during the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class ProgressReporter < Reporter
-
1
def prerecord klass, name #:nodoc:
-
if options[:verbose] then
-
io.print "%s#%s = " % [klass.name, name]
-
io.flush
-
end
-
end
-
-
1
def record result # :nodoc:
-
io.print "%.2f s = " % [result.time] if options[:verbose]
-
io.print result.result_code
-
io.puts if options[:verbose]
-
end
-
end
-
-
##
-
# A reporter that gathers statistics about a test run. Does not do
-
# any IO because meant to be used as a parent class for a reporter
-
# that does.
-
#
-
# If you want to create an entirely different type of output (eg,
-
# CI, HTML, etc), this is the place to start.
-
#
-
# Example:
-
#
-
# class JenkinsCIReporter < StatisticsReporter
-
# def report
-
# super # Needed to calculate some statistics
-
#
-
# print "<testsuite "
-
# print "tests='#{count}' "
-
# print "failures='#{failures}' "
-
# # Remaining XML...
-
# end
-
# end
-
-
1
class StatisticsReporter < Reporter
-
##
-
# Total number of assertions.
-
-
1
attr_accessor :assertions
-
-
##
-
# Total number of test cases.
-
-
1
attr_accessor :count
-
-
##
-
# An +Array+ of test cases that failed or were skipped.
-
-
1
attr_accessor :results
-
-
##
-
# Time the test run started. If available, the monotonic clock is
-
# used and this is a +Float+, otherwise it's an instance of
-
# +Time+.
-
-
1
attr_accessor :start_time
-
-
##
-
# Test run time. If available, the monotonic clock is used and
-
# this is a +Float+, otherwise it's an instance of +Time+.
-
-
1
attr_accessor :total_time
-
-
##
-
# Total number of tests that failed.
-
-
1
attr_accessor :failures
-
-
##
-
# Total number of tests that erred.
-
-
1
attr_accessor :errors
-
-
##
-
# Total number of tests that where skipped.
-
-
1
attr_accessor :skips
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
super
-
-
self.assertions = 0
-
self.count = 0
-
self.results = []
-
self.start_time = nil
-
self.total_time = nil
-
self.failures = nil
-
self.errors = nil
-
self.skips = nil
-
end
-
-
1
def passed? # :nodoc:
-
results.all?(&:skipped?)
-
end
-
-
1
def start # :nodoc:
-
self.start_time = Minitest.clock_time
-
end
-
-
1
def record result # :nodoc:
-
self.count += 1
-
self.assertions += result.assertions
-
-
results << result if not result.passed? or result.skipped?
-
end
-
-
##
-
# Report on the tracked statistics.
-
-
1
def report
-
aggregate = results.group_by { |r| r.failure.class }
-
aggregate.default = [] # dumb. group_by should provide this
-
-
self.total_time = Minitest.clock_time - start_time
-
self.failures = aggregate[Assertion].size
-
self.errors = aggregate[UnexpectedError].size
-
self.skips = aggregate[Skip].size
-
end
-
end
-
-
##
-
# A reporter that prints the header, summary, and failure details at
-
# the end of the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class SummaryReporter < StatisticsReporter
-
# :stopdoc:
-
1
attr_accessor :sync
-
1
attr_accessor :old_sync
-
# :startdoc:
-
-
1
def start # :nodoc:
-
super
-
-
io.puts "Run options: #{options[:args]}"
-
io.puts
-
io.puts "# Running:"
-
io.puts
-
-
self.sync = io.respond_to? :"sync=" # stupid emacs
-
self.old_sync, io.sync = io.sync, true if self.sync
-
end
-
-
1
def report # :nodoc:
-
super
-
-
io.sync = self.old_sync
-
-
io.puts unless options[:verbose] # finish the dots
-
io.puts
-
io.puts statistics
-
aggregated_results io
-
io.puts summary
-
end
-
-
1
def statistics # :nodoc:
-
"Finished in %.6fs, %.4f runs/s, %.4f assertions/s." %
-
[total_time, count / total_time, assertions / total_time]
-
end
-
-
1
def aggregated_results io # :nodoc:
-
filtered_results = results.dup
-
filtered_results.reject!(&:skipped?) unless options[:verbose]
-
-
filtered_results.each_with_index { |result, i|
-
io.puts "\n%3d) %s" % [i+1, result]
-
}
-
io.puts
-
io
-
end
-
-
1
def to_s # :nodoc:
-
aggregated_results(StringIO.new(binary_string)).string
-
end
-
-
1
def summary # :nodoc:
-
extra = ""
-
-
extra = "\n\nYou have skipped tests. Run with --verbose for details." if
-
results.any?(&:skipped?) unless options[:verbose] or ENV["MT_NO_SKIP_MSG"]
-
-
"%d runs, %d assertions, %d failures, %d errors, %d skips%s" %
-
[count, assertions, failures, errors, skips, extra]
-
end
-
-
1
private
-
-
1
if '<3'.respond_to? :b
-
1
def binary_string; ''.b; end
-
else
-
def binary_string; ''.force_encoding(Encoding::ASCII_8BIT); end
-
end
-
end
-
-
##
-
# Dispatch to multiple reporters as one.
-
-
1
class CompositeReporter < AbstractReporter
-
##
-
# The list of reporters to dispatch to.
-
-
1
attr_accessor :reporters
-
-
1
def initialize *reporters # :nodoc:
-
super()
-
self.reporters = reporters
-
end
-
-
1
def io # :nodoc:
-
reporters.first.io
-
end
-
-
##
-
# Add another reporter to the mix.
-
-
1
def << reporter
-
self.reporters << reporter
-
end
-
-
1
def passed? # :nodoc:
-
self.reporters.all?(&:passed?)
-
end
-
-
1
def start # :nodoc:
-
self.reporters.each(&:start)
-
end
-
-
1
def prerecord klass, name # :nodoc:
-
self.reporters.each do |reporter|
-
# TODO: remove conditional for minitest 6
-
reporter.prerecord klass, name if reporter.respond_to? :prerecord
-
end
-
end
-
-
1
def record result # :nodoc:
-
self.reporters.each do |reporter|
-
reporter.record result
-
end
-
end
-
-
1
def report # :nodoc:
-
self.reporters.each(&:report)
-
end
-
end
-
-
##
-
# Represents run failures.
-
-
1
class Assertion < Exception
-
1
def error # :nodoc:
-
self
-
end
-
-
##
-
# Where was this run before an assertion was raised?
-
-
1
def location
-
last_before_assertion = ""
-
self.backtrace.reverse_each do |s|
-
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
-
last_before_assertion = s
-
end
-
last_before_assertion.sub(/:in .*$/, "")
-
end
-
-
1
def result_code # :nodoc:
-
result_label[0, 1]
-
end
-
-
1
def result_label # :nodoc:
-
"Failure"
-
end
-
end
-
-
##
-
# Assertion raised when skipping a run.
-
-
1
class Skip < Assertion
-
1
def result_label # :nodoc:
-
"Skipped"
-
end
-
end
-
-
##
-
# Assertion wrapping an unexpected error that was raised during a run.
-
-
1
class UnexpectedError < Assertion
-
# TODO: figure out how to use `cause` instead
-
1
attr_accessor :error # :nodoc:
-
-
1
def initialize error # :nodoc:
-
super "Unexpected exception"
-
self.error = error
-
end
-
-
1
def backtrace # :nodoc:
-
self.error.backtrace
-
end
-
-
1
def message # :nodoc:
-
bt = Minitest.filter_backtrace(self.backtrace).join "\n "
-
"#{self.error.class}: #{self.error.message}\n #{bt}"
-
end
-
-
1
def result_label # :nodoc:
-
"Error"
-
end
-
end
-
-
##
-
# Provides a simple set of guards that you can use in your tests
-
# to skip execution if it is not applicable. These methods are
-
# mixed into Test as both instance and class methods so you
-
# can use them inside or outside of the test methods.
-
#
-
# def test_something_for_mri
-
# skip "bug 1234" if jruby?
-
# # ...
-
# end
-
#
-
# if windows? then
-
# # ... lots of test methods ...
-
# end
-
-
1
module Guard
-
-
##
-
# Is this running on jruby?
-
-
1
def jruby? platform = RUBY_PLATFORM
-
"java" == platform
-
end
-
-
##
-
# Is this running on maglev?
-
-
1
def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `maglev?` called from #{where}. This will fail in Minitest 6."
-
"maglev" == platform
-
end
-
-
##
-
# Is this running on mri?
-
-
1
def mri? platform = RUBY_DESCRIPTION
-
/^ruby/ =~ platform
-
end
-
-
##
-
# Is this running on macOS?
-
-
1
def osx? platform = RUBY_PLATFORM
-
/darwin/ =~ platform
-
end
-
-
##
-
# Is this running on rubinius?
-
-
1
def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `rubinius?` called from #{where}. This will fail in Minitest 6."
-
"rbx" == platform
-
end
-
-
##
-
# Is this running on windows?
-
-
1
def windows? platform = RUBY_PLATFORM
-
/mswin|mingw/ =~ platform
-
end
-
end
-
-
##
-
# The standard backtrace filter for minitest.
-
#
-
# See Minitest.backtrace_filter=.
-
-
1
class BacktraceFilter
-
-
1
MT_RE = %r%lib/minitest% #:nodoc:
-
-
##
-
# Filter +bt+ to something useful. Returns the whole thing if
-
# $DEBUG (ruby) or $MT_DEBUG (env).
-
-
1
def filter bt
-
return ["No backtrace"] unless bt
-
-
return bt.dup if $DEBUG || ENV["MT_DEBUG"]
-
-
new_bt = bt.take_while { |line| line !~ MT_RE }
-
new_bt = bt.select { |line| line !~ MT_RE } if new_bt.empty?
-
new_bt = bt.dup if new_bt.empty?
-
-
new_bt
-
end
-
end
-
-
1
self.backtrace_filter = BacktraceFilter.new
-
-
1
def self.run_one_method klass, method_name # :nodoc:
-
result = klass.new(method_name).run
-
raise "#{klass}#run _must_ return a Result" unless Result === result
-
result
-
end
-
-
# :stopdoc:
-
-
1
if defined? Process::CLOCK_MONOTONIC # :nodoc:
-
1
def self.clock_time
-
Process.clock_gettime Process::CLOCK_MONOTONIC
-
end
-
else
-
def self.clock_time
-
Time.now
-
end
-
end
-
-
1
class Runnable # re-open
-
1
def self.inherited klass # :nodoc:
-
9
self.runnables << klass
-
9
super
-
end
-
end
-
-
# :startdoc:
-
end
-
-
1
require "minitest/test"
-
# encoding: UTF-8
-
-
1
require "rbconfig"
-
1
require "tempfile"
-
1
require "stringio"
-
-
1
module Minitest
-
##
-
# Minitest Assertions. All assertion methods accept a +msg+ which is
-
# printed if the assertion fails.
-
#
-
# Protocol: Nearly everything here boils up to +assert+, which
-
# expects to be able to increment an instance accessor named
-
# +assertions+. This is not provided by Assertions and must be
-
# provided by the thing including Assertions. See Minitest::Runnable
-
# for an example.
-
-
1
module Assertions
-
1
UNDEFINED = Object.new # :nodoc:
-
-
1
def UNDEFINED.inspect # :nodoc:
-
"UNDEFINED" # again with the rdoc bugs... :(
-
end
-
-
##
-
# Returns the diff command to use in #diff. Tries to intelligently
-
# figure out what diff to use.
-
-
1
def self.diff
-
return @diff if defined? @diff
-
-
@diff = if (RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ &&
-
system("diff.exe", __FILE__, __FILE__)) then
-
"diff.exe -u"
-
elsif system("gdiff", __FILE__, __FILE__)
-
"gdiff -u" # solaris and kin suck
-
elsif system("diff", __FILE__, __FILE__)
-
"diff -u"
-
else
-
nil
-
end
-
end
-
-
##
-
# Set the diff command to use in #diff.
-
-
1
def self.diff= o
-
@diff = o
-
end
-
-
##
-
# Returns a diff between +exp+ and +act+. If there is no known
-
# diff command or if it doesn't make sense to diff the output
-
# (single line, short output), then it simply returns a basic
-
# comparison between the two.
-
#
-
# See +things_to_diff+ for more info.
-
-
1
def diff exp, act
-
result = nil
-
-
expect, butwas = things_to_diff(exp, act)
-
-
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
-
expect
-
-
Tempfile.open("expect") do |a|
-
a.puts expect
-
a.flush
-
-
Tempfile.open("butwas") do |b|
-
b.puts butwas
-
b.flush
-
-
result = `#{Minitest::Assertions.diff} #{a.path} #{b.path}`
-
result.sub!(/^\-\-\- .+/, "--- expected")
-
result.sub!(/^\+\+\+ .+/, "+++ actual")
-
-
if result.empty? then
-
klass = exp.class
-
result = [
-
"No visible difference in the #{klass}#inspect output.\n",
-
"You should look at the implementation of #== on ",
-
"#{klass} or its members.\n",
-
expect,
-
].join
-
end
-
end
-
end
-
-
result
-
end
-
-
##
-
# Returns things to diff [expect, butwas], or [nil, nil] if nothing to diff.
-
#
-
# Criterion:
-
#
-
# 1. Strings include newlines or escaped newlines, but not both.
-
# 2. or: String lengths are > 30 characters.
-
# 3. or: Strings are equal to each other (but maybe different encodings?).
-
# 4. and: we found a diff executable.
-
-
1
def things_to_diff exp, act
-
expect = mu_pp_for_diff exp
-
butwas = mu_pp_for_diff act
-
-
e1, e2 = expect.include?("\n"), expect.include?("\\n")
-
b1, b2 = butwas.include?("\n"), butwas.include?("\\n")
-
-
need_to_diff =
-
(e1 ^ e2 ||
-
b1 ^ b2 ||
-
expect.size > 30 ||
-
butwas.size > 30 ||
-
expect == butwas) &&
-
Minitest::Assertions.diff
-
-
need_to_diff && [expect, butwas]
-
end
-
-
##
-
# This returns a human-readable version of +obj+. By default
-
# #inspect is called. You can override this to use #pretty_inspect
-
# if you want.
-
#
-
# See Minitest::Test.make_my_diffs_pretty!
-
-
1
def mu_pp obj
-
s = obj.inspect
-
-
if defined? Encoding then
-
s = s.encode Encoding.default_external
-
-
if String === obj && (obj.encoding != Encoding.default_external ||
-
!obj.valid_encoding?) then
-
enc = "# encoding: #{obj.encoding}"
-
val = "# valid: #{obj.valid_encoding?}"
-
s = "#{enc}\n#{val}\n#{s}"
-
end
-
end
-
-
s
-
end
-
-
##
-
# This returns a diff-able more human-readable version of +obj+.
-
# This differs from the regular mu_pp because it expands escaped
-
# newlines and makes hex-values (like object_ids) generic. This
-
# uses mu_pp to do the first pass and then cleans it up.
-
-
1
def mu_pp_for_diff obj
-
str = mu_pp obj
-
-
# both '\n' & '\\n' (_after_ mu_pp (aka inspect))
-
single = !!str.match(/(?<!\\|^)\\n/)
-
double = !!str.match(/(?<=\\|^)\\n/)
-
-
process =
-
if single ^ double then
-
if single then
-
lambda { |s| s == "\\n" ? "\n" : s } # unescape
-
else
-
lambda { |s| s == "\\\\n" ? "\\n\n" : s } # unescape a bit, add nls
-
end
-
else
-
:itself # leave it alone
-
end
-
-
str.
-
gsub(/\\?\\n/, &process).
-
gsub(/:0x[a-fA-F0-9]{4,}/m, ":0xXXXXXX") # anonymize hex values
-
end
-
-
##
-
# Fails unless +test+ is truthy.
-
-
1
def assert test, msg = nil
-
self.assertions += 1
-
unless test then
-
msg ||= "Expected #{mu_pp test} to be truthy."
-
msg = msg.call if Proc === msg
-
raise Minitest::Assertion, msg
-
end
-
true
-
end
-
-
1
def _synchronize # :nodoc:
-
yield
-
end
-
-
##
-
# Fails unless +obj+ is empty.
-
-
1
def assert_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
-
assert_respond_to obj, :empty?
-
assert obj.empty?, msg
-
end
-
-
1
E = "" # :nodoc:
-
-
##
-
# Fails unless <tt>exp == act</tt> printing the difference between
-
# the two, if possible.
-
#
-
# If there is no visible difference but the assertion fails, you
-
# should suspect that your #== is buggy, or your inspect output is
-
# missing crucial details. For nicer structural diffing, set
-
# Minitest::Test.make_my_diffs_pretty!
-
#
-
# For floats use assert_in_delta.
-
#
-
# See also: Minitest::Assertions.diff
-
-
1
def assert_equal exp, act, msg = nil
-
msg = message(msg, E) { diff exp, act }
-
result = assert exp == act, msg
-
-
if nil == exp then
-
if Minitest::VERSION =~ /^6/ then
-
refute_nil exp, "Use assert_nil if expecting nil."
-
else
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
-
warn "DEPRECATED: Use assert_nil if expecting nil from #{where}. This will fail in Minitest 6."
-
end
-
end
-
-
result
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
-
# of each other.
-
#
-
# assert_in_delta Math::PI, (22.0 / 7.0), 0.01
-
-
1
def assert_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
-
}
-
assert delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ have a relative
-
# error less than +epsilon+.
-
-
1
def assert_in_epsilon exp, act, epsilon = 0.001, msg = nil
-
assert_in_delta exp, act, [exp.abs, act.abs].min * epsilon, msg
-
end
-
-
##
-
# Fails unless +collection+ includes +obj+.
-
-
1
def assert_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
assert collection.include?(obj), msg
-
end
-
-
##
-
# Fails unless +obj+ is an instance of +cls+.
-
-
1
def assert_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
-
}
-
-
assert obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails unless +obj+ is a kind of +cls+.
-
-
1
def assert_kind_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
-
-
assert obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails unless +matcher+ <tt>=~</tt> +obj+.
-
-
1
def assert_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
assert matcher =~ obj, msg
-
end
-
-
##
-
# Fails unless +obj+ is nil
-
-
1
def assert_nil obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
-
assert obj.nil?, msg
-
end
-
-
##
-
# For testing with binary operators. Eg:
-
#
-
# assert_operator 5, :<=, 4
-
-
1
def assert_operator o1, op, o2 = UNDEFINED, msg = nil
-
return assert_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
-
assert o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if stdout or stderr do not output the expected results.
-
# Pass in nil if you don't care about that streams output. Pass in
-
# "" if you require it to be silent. Pass in a regexp if you want
-
# to pattern match.
-
#
-
# assert_output(/hey/) { method_with_output }
-
#
-
# NOTE: this uses #capture_io, not #capture_subprocess_io.
-
#
-
# See also: #assert_silent
-
-
1
def assert_output stdout = nil, stderr = nil
-
flunk "assert_output requires a block to capture output." unless
-
block_given?
-
-
out, err = capture_io do
-
yield
-
end
-
-
err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
-
out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
-
-
y = send err_msg, stderr, err, "In stderr" if err_msg
-
x = send out_msg, stdout, out, "In stdout" if out_msg
-
-
(!stdout || x) && (!stderr || y)
-
rescue Assertion
-
raise
-
rescue => e
-
raise UnexpectedError, e
-
end
-
-
##
-
# Fails unless +path+ exists.
-
-
1
def assert_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to exist" }
-
assert File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates. Eg:
-
#
-
# assert_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by assert_operator:
-
#
-
# str.must_be :empty?
-
-
1
def assert_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
-
assert o1.__send__(op), msg
-
end
-
-
##
-
# Fails unless the block raises one of +exp+. Returns the
-
# exception matched so you can check the message, attributes, etc.
-
#
-
# +exp+ takes an optional message on the end to help explain
-
# failures and defaults to StandardError if no exception class is
-
# passed. Eg:
-
#
-
# assert_raises(CustomError) { method_with_custom_error }
-
#
-
# With custom error message:
-
#
-
# assert_raises(CustomError, 'This should have raised CustomError') { method_with_custom_error }
-
#
-
# Using the returned object:
-
#
-
# error = assert_raises(CustomError) do
-
# raise CustomError, 'This is really bad'
-
# end
-
#
-
# assert_equal 'This is really bad', error.message
-
-
1
def assert_raises *exp
-
flunk "assert_raises requires a block to capture errors." unless
-
block_given?
-
-
msg = "#{exp.pop}.\n" if String === exp.last
-
exp << StandardError if exp.empty?
-
-
begin
-
yield
-
rescue *exp => e
-
pass # count assertion
-
return e
-
rescue Minitest::Assertion # incl Skip & UnexpectedError
-
# don't count assertion
-
raise
-
rescue SignalException, SystemExit
-
raise
-
rescue Exception => e
-
flunk proc {
-
exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
-
}
-
end
-
-
exp = exp.first if exp.size == 1
-
-
flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
-
end
-
-
##
-
# Fails unless +obj+ responds to +meth+.
-
-
1
def assert_respond_to obj, meth, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
-
}
-
assert obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails unless +exp+ and +act+ are #equal?
-
-
1
def assert_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
-
}
-
assert exp.equal?(act), msg
-
end
-
-
##
-
# +send_ary+ is a receiver, message and arguments.
-
#
-
# Fails unless the call returns a true value
-
-
1
def assert_send send_ary, m = nil
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: assert_send. From #{where}"
-
-
recv, msg, *args = send_ary
-
m = message(m) {
-
"Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
-
assert recv.__send__(msg, *args), m
-
end
-
-
##
-
# Fails if the block outputs anything to stderr or stdout.
-
#
-
# See also: #assert_output
-
-
1
def assert_silent
-
assert_output "", "" do
-
yield
-
end
-
end
-
-
##
-
# Fails unless the block throws +sym+
-
-
1
def assert_throws sym, msg = nil
-
default = "Expected #{mu_pp(sym)} to have been thrown"
-
caught = true
-
catch(sym) do
-
begin
-
yield
-
rescue ThreadError => e # wtf?!? 1.8 + threads == suck
-
default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
-
rescue ArgumentError => e # 1.9 exception
-
raise e unless e.message.include?("uncaught throw")
-
default += ", not #{e.message.split(/ /).last}"
-
rescue NameError => e # 1.8 exception
-
raise e unless e.name == sym
-
default += ", not #{e.name.inspect}"
-
end
-
caught = false
-
end
-
-
assert caught, message(msg) { default }
-
rescue Assertion
-
raise
-
rescue => e
-
raise UnexpectedError, e
-
end
-
-
##
-
# Captures $stdout and $stderr into strings:
-
#
-
# out, err = capture_io do
-
# puts "Some info"
-
# warn "You did a bad thing"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: For efficiency, this method uses StringIO and does not
-
# capture IO for subprocesses. Use #capture_subprocess_io for
-
# that.
-
-
1
def capture_io
-
_synchronize do
-
begin
-
captured_stdout, captured_stderr = StringIO.new, StringIO.new
-
-
orig_stdout, orig_stderr = $stdout, $stderr
-
$stdout, $stderr = captured_stdout, captured_stderr
-
-
yield
-
-
return captured_stdout.string, captured_stderr.string
-
ensure
-
$stdout = orig_stdout
-
$stderr = orig_stderr
-
end
-
end
-
end
-
-
##
-
# Captures $stdout and $stderr into strings, using Tempfile to
-
# ensure that subprocess IO is captured as well.
-
#
-
# out, err = capture_subprocess_io do
-
# system "echo Some info"
-
# system "echo You did a bad thing 1>&2"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: This method is approximately 10x slower than #capture_io so
-
# only use it when you need to test the output of a subprocess.
-
-
1
def capture_subprocess_io
-
_synchronize do
-
begin
-
require "tempfile"
-
-
captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err")
-
-
orig_stdout, orig_stderr = $stdout.dup, $stderr.dup
-
$stdout.reopen captured_stdout
-
$stderr.reopen captured_stderr
-
-
yield
-
-
$stdout.rewind
-
$stderr.rewind
-
-
return captured_stdout.read, captured_stderr.read
-
ensure
-
captured_stdout.unlink
-
captured_stderr.unlink
-
$stdout.reopen orig_stdout
-
$stderr.reopen orig_stderr
-
-
orig_stdout.close
-
orig_stderr.close
-
captured_stdout.close
-
captured_stderr.close
-
end
-
end
-
end
-
-
##
-
# Returns details for exception +e+
-
-
1
def exception_details e, msg
-
[
-
"#{msg}",
-
"Class: <#{e.class}>",
-
"Message: <#{e.message.inspect}>",
-
"---Backtrace---",
-
"#{Minitest.filter_backtrace(e.backtrace).join("\n")}",
-
"---------------",
-
].join "\n"
-
end
-
-
##
-
# Fails after a given date (in the local time zone). This allows
-
# you to put time-bombs in your tests if you need to keep
-
# something around until a later date lest you forget about it.
-
-
1
def fail_after y,m,d,msg
-
flunk msg if Time.now > Time.local(y, m, d)
-
end
-
-
##
-
# Fails with +msg+.
-
-
1
def flunk msg = nil
-
msg ||= "Epic Fail!"
-
assert false, msg
-
end
-
-
##
-
# Returns a proc that will output +msg+ along with the default message.
-
-
1
def message msg = nil, ending = nil, &default
-
proc {
-
msg = msg.call.chomp(".") if Proc === msg
-
custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
-
"#{custom_message}#{default.call}#{ending || "."}"
-
}
-
end
-
-
##
-
# used for counting assertions
-
-
1
def pass _msg = nil
-
assert true
-
end
-
-
##
-
# Fails if +test+ is truthy.
-
-
1
def refute test, msg = nil
-
msg ||= message { "Expected #{mu_pp(test)} to not be truthy" }
-
assert !test, msg
-
end
-
-
##
-
# Fails if +obj+ is empty.
-
-
1
def refute_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
-
assert_respond_to obj, :empty?
-
refute obj.empty?, msg
-
end
-
-
##
-
# Fails if <tt>exp == act</tt>.
-
#
-
# For floats use refute_in_delta.
-
-
1
def refute_equal exp, act, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
-
}
-
refute exp == act, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
-
#
-
# refute_in_delta Math::PI, (22.0 / 7.0)
-
-
1
def refute_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
-
}
-
refute delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
-
# less than +epsilon+.
-
-
1
def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
-
refute_in_delta a, b, a * epsilon, msg
-
end
-
-
##
-
# Fails if +collection+ includes +obj+.
-
-
1
def refute_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
refute collection.include?(obj), msg
-
end
-
-
##
-
# Fails if +obj+ is an instance of +cls+.
-
-
1
def refute_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to not be an instance of #{cls}"
-
}
-
refute obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails if +obj+ is a kind of +cls+.
-
-
1
def refute_kind_of cls, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
-
refute obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails if +matcher+ <tt>=~</tt> +obj+.
-
-
1
def refute_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
refute matcher =~ obj, msg
-
end
-
-
##
-
# Fails if +obj+ is nil.
-
-
1
def refute_nil obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
-
refute obj.nil?, msg
-
end
-
-
##
-
# Fails if +o1+ is not +op+ +o2+. Eg:
-
#
-
# refute_operator 1, :>, 2 #=> pass
-
# refute_operator 1, :<, 2 #=> fail
-
-
1
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
-
return refute_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}" }
-
refute o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if +path+ exists.
-
-
1
def refute_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to not exist" }
-
refute File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates.
-
#
-
# refute_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by refute_operator:
-
#
-
# str.wont_be :empty?
-
-
1
def refute_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
-
refute o1.__send__(op), msg
-
end
-
-
##
-
# Fails if +obj+ responds to the message +meth+.
-
-
1
def refute_respond_to obj, meth, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
-
-
refute obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails if +exp+ is the same (by object identity) as +act+.
-
-
1
def refute_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
-
}
-
refute exp.equal?(act), msg
-
end
-
-
##
-
# Skips the current run. If run in verbose-mode, the skipped run
-
# gets listed at the end of the run but doesn't cause a failure
-
# exit code.
-
-
1
def skip msg = nil, bt = caller
-
msg ||= "Skipped, no message given"
-
@skip = true
-
raise Minitest::Skip, msg, bt
-
end
-
-
##
-
# Skips the current run until a given date (in the local time
-
# zone). This allows you to put some fixes on hold until a later
-
# date, but still holds you accountable and prevents you from
-
# forgetting it.
-
-
1
def skip_until y,m,d,msg
-
skip msg if Time.now < Time.local(y, m, d)
-
where = caller.first.split(/:/, 3).first(2).join ":"
-
warn "Stale skip_until %p at %s" % [msg, where]
-
end
-
-
##
-
# Was this testcase skipped? Meant for #teardown.
-
-
1
def skipped?
-
defined?(@skip) and @skip
-
end
-
end
-
end
-
1
module Minitest
-
1
module Parallel #:nodoc:
-
-
##
-
# The engine used to run multiple tests in parallel.
-
-
1
class Executor
-
-
##
-
# The size of the pool of workers.
-
-
1
attr_reader :size
-
-
##
-
# Create a parallel test executor of with +size+ workers.
-
-
1
def initialize size
-
1
@size = size
-
1
@queue = Queue.new
-
1
@pool = nil
-
end
-
-
##
-
# Start the executor
-
-
1
def start
-
@pool = size.times.map {
-
Thread.new(@queue) do |queue|
-
Thread.current.abort_on_exception = true
-
while (job = queue.pop)
-
klass, method, reporter = job
-
reporter.synchronize { reporter.prerecord klass, method }
-
result = Minitest.run_one_method klass, method
-
reporter.synchronize { reporter.record result }
-
end
-
end
-
}
-
end
-
-
##
-
# Add a job to the queue
-
-
1
def << work; @queue << work; end
-
-
##
-
# Shuts down the pool of workers by signalling them to quit and
-
# waiting for them all to finish what they're currently working
-
# on.
-
-
1
def shutdown
-
size.times { @queue << nil }
-
@pool.each(&:join)
-
end
-
end
-
-
1
module Test # :nodoc:
-
1
def _synchronize; Minitest::Test.io_lock.synchronize { yield }; end # :nodoc:
-
-
1
module ClassMethods # :nodoc:
-
1
def run_one_method klass, method_name, reporter
-
Minitest.parallel_executor << [klass, method_name, reporter]
-
end
-
-
1
def test_order
-
:parallel
-
end
-
end
-
end
-
end
-
end
-
1
require "minitest" unless defined? Minitest::Runnable
-
-
1
module Minitest
-
##
-
# Subclass Test to create your own tests. Typically you'll want a
-
# Test subclass per implementation class.
-
#
-
# See Minitest::Assertions
-
-
1
class Test < Runnable
-
1
require "minitest/assertions"
-
1
include Minitest::Assertions
-
1
include Minitest::Reportable
-
-
1
def class_name # :nodoc:
-
self.class.name # for Minitest::Reportable
-
end
-
-
1
PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, SystemExit] # :nodoc:
-
-
# :stopdoc:
-
2
class << self; attr_accessor :io_lock; end
-
1
self.io_lock = Mutex.new
-
# :startdoc:
-
-
##
-
# Call this at the top of your tests when you absolutely
-
# positively need to have ordered tests. In doing so, you're
-
# admitting that you suck and your tests are weak.
-
-
1
def self.i_suck_and_my_tests_are_order_dependent!
-
class << self
-
undef_method :test_order if method_defined? :test_order
-
define_method :test_order do :alpha end
-
end
-
end
-
-
##
-
# Make diffs for this Test use #pretty_inspect so that diff
-
# in assert_equal can have more details. NOTE: this is much slower
-
# than the regular inspect but much more usable for complex
-
# objects.
-
-
1
def self.make_my_diffs_pretty!
-
require "pp"
-
-
define_method :mu_pp, &:pretty_inspect
-
end
-
-
##
-
# Call this at the top of your tests when you want to run your
-
# tests in parallel. In doing so, you're admitting that you rule
-
# and your tests are awesome.
-
-
1
def self.parallelize_me!
-
include Minitest::Parallel::Test
-
extend Minitest::Parallel::Test::ClassMethods
-
end
-
-
##
-
# Returns all instance methods starting with "test_". Based on
-
# #test_order, the methods are either sorted, randomized
-
# (default), or run in parallel.
-
-
1
def self.runnable_methods
-
methods = methods_matching(/^test_/)
-
-
case self.test_order
-
when :random, :parallel then
-
max = methods.size
-
methods.sort.sort_by { rand max }
-
when :alpha, :sorted then
-
methods.sort
-
else
-
raise "Unknown test_order: #{self.test_order.inspect}"
-
end
-
end
-
-
##
-
# Defines the order to run tests (:random by default). Override
-
# this or use a convenience method to change it for your tests.
-
-
1
def self.test_order
-
:random
-
end
-
-
1
TEARDOWN_METHODS = %w[ before_teardown teardown after_teardown ] # :nodoc:
-
-
##
-
# Runs a single test with setup/teardown hooks.
-
-
1
def run
-
with_info_handler do
-
time_it do
-
capture_exceptions do
-
before_setup; setup; after_setup
-
-
self.send self.name
-
end
-
-
TEARDOWN_METHODS.each do |hook|
-
capture_exceptions do
-
self.send hook
-
end
-
end
-
end
-
end
-
-
Result.from self # per contract
-
end
-
-
##
-
# Provides before/after hooks for setup and teardown. These are
-
# meant for library writers, NOT for regular test authors. See
-
# #before_setup for an example.
-
-
1
module LifecycleHooks
-
-
##
-
# Runs before every test, before setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# As a simplistic example:
-
#
-
# module MyMinitestPlugin
-
# def before_setup
-
# super
-
# # ... stuff to do before setup is run
-
# end
-
#
-
# def after_setup
-
# # ... stuff to do after setup is run
-
# super
-
# end
-
#
-
# def before_teardown
-
# super
-
# # ... stuff to do before teardown is run
-
# end
-
#
-
# def after_teardown
-
# # ... stuff to do after teardown is run
-
# super
-
# end
-
# end
-
#
-
# class MiniTest::Test
-
# include MyMinitestPlugin
-
# end
-
-
1
def before_setup; end
-
-
##
-
# Runs before every test. Use this to set up before each test
-
# run.
-
-
1
def setup; end
-
-
##
-
# Runs before every test, after setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_setup; end
-
-
##
-
# Runs after every test, before teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def before_teardown; end
-
-
##
-
# Runs after every test. Use this to clean up after each test
-
# run.
-
-
1
def teardown; end
-
-
##
-
# Runs after every test, after teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_teardown; end
-
end # LifecycleHooks
-
-
1
def capture_exceptions # :nodoc:
-
yield
-
rescue *PASSTHROUGH_EXCEPTIONS
-
raise
-
rescue Assertion => e
-
self.failures << e
-
rescue Exception => e
-
self.failures << UnexpectedError.new(e)
-
end
-
-
1
def with_info_handler &block # :nodoc:
-
t0 = Minitest.clock_time
-
-
handler = lambda do
-
warn "\nCurrent: %s#%s %.2fs" % [self.class, self.name, Minitest.clock_time - t0]
-
end
-
-
self.class.on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
include LifecycleHooks
-
1
include Guard
-
1
extend Guard
-
end # Test
-
end
-
-
1
require "minitest/unit" unless defined?(MiniTest) # compatibility layer only
-
# :stopdoc:
-
-
1
unless defined?(Minitest) then
-
# all of this crap is just to avoid circular requires and is only
-
# needed if a user requires "minitest/unit" directly instead of
-
# "minitest/autorun", so we also warn
-
-
from = caller.reject { |s| s =~ /rubygems/ }.join("\n ")
-
warn "Warning: you should require 'minitest/autorun' instead."
-
warn %(Warning: or add 'gem "minitest"' before 'require "minitest/autorun"')
-
warn "From:\n #{from}"
-
-
module Minitest; end
-
MiniTest = Minitest # prevents minitest.rb from requiring back to us
-
require "minitest"
-
end
-
-
1
MiniTest = Minitest unless defined?(MiniTest)
-
-
1
module Minitest
-
1
class Unit
-
1
VERSION = Minitest::VERSION
-
1
class TestCase < Minitest::Test
-
1
def self.inherited klass # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit::TestCase is now Minitest::Test. From #{from}"
-
super
-
end
-
end
-
-
1
def self.autorun # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.autorun is now Minitest.autorun. From #{from}"
-
Minitest.autorun
-
end
-
-
1
def self.after_tests &b # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.after_tests is now Minitest.after_run. From #{from}"
-
Minitest.after_run(&b)
-
end
-
end
-
end
-
-
# :startdoc:
-
# frozen_string_literal: true
-
-
1
module Rack
-
-
# Middleware that applies chunked transfer encoding to response bodies
-
# when the response does not include a Content-Length header.
-
#
-
# This supports the Trailer response header to allow the use of trailing
-
# headers in the chunked encoding. However, using this requires you manually
-
# specify a response body that supports a +trailers+ method. Example:
-
#
-
# [200, { 'Trailer' => 'Expires'}, ["Hello", "World"]]
-
# # error raised
-
#
-
# body = ["Hello", "World"]
-
# def body.trailers
-
# { 'Expires' => Time.now.to_s }
-
# end
-
# [200, { 'Trailer' => 'Expires'}, body]
-
# # No exception raised
-
1
class Chunked
-
1
include Rack::Utils
-
-
# A body wrapper that emits chunked responses.
-
1
class Body
-
1
TERM = "\r\n"
-
1
TAIL = "0#{TERM}"
-
-
# Store the response body to be chunked.
-
1
def initialize(body)
-
@body = body
-
end
-
-
# For each element yielded by the response body, yield
-
# the element in chunked encoding.
-
1
def each(&block)
-
term = TERM
-
@body.each do |chunk|
-
size = chunk.bytesize
-
next if size == 0
-
-
yield [size.to_s(16), term, chunk.b, term].join
-
end
-
yield TAIL
-
yield_trailers(&block)
-
yield term
-
end
-
-
# Close the response body if the response body supports it.
-
1
def close
-
@body.close if @body.respond_to?(:close)
-
end
-
-
1
private
-
-
# Do nothing as this class does not support trailer headers.
-
1
def yield_trailers
-
end
-
end
-
-
# A body wrapper that emits chunked responses and also supports
-
# sending Trailer headers. Note that the response body provided to
-
# initialize must have a +trailers+ method that returns a hash
-
# of trailer headers, and the rack response itself should have a
-
# Trailer header listing the headers that the +trailers+ method
-
# will return.
-
1
class TrailerBody < Body
-
1
private
-
-
# Yield strings for each trailer header.
-
1
def yield_trailers
-
@body.trailers.each_pair do |k, v|
-
yield "#{k}: #{v}\r\n"
-
end
-
end
-
end
-
-
1
def initialize(app)
-
@app = app
-
end
-
-
# Whether the HTTP version supports chunked encoding (HTTP 1.1 does).
-
1
def chunkable_version?(ver)
-
case ver
-
# pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
-
# a version (nor response headers)
-
when 'HTTP/1.0', nil, 'HTTP/0.9'
-
false
-
else
-
true
-
end
-
end
-
-
# If the rack app returns a response that should have a body,
-
# but does not have Content-Length or Transfer-Encoding headers,
-
# modify the response to use chunked Transfer-Encoding.
-
1
def call(env)
-
status, headers, body = @app.call(env)
-
headers = HeaderHash[headers]
-
-
if chunkable_version?(env[SERVER_PROTOCOL]) &&
-
!STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
-
!headers[CONTENT_LENGTH] &&
-
!headers[TRANSFER_ENCODING]
-
-
headers[TRANSFER_ENCODING] = 'chunked'
-
if headers['Trailer']
-
body = TrailerBody.new(body)
-
else
-
body = Body.new(body)
-
end
-
end
-
-
[status, headers, body]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'uri'
-
1
require 'stringio'
-
1
require_relative '../rack'
-
1
require 'cgi/cookie'
-
-
1
module Rack
-
# Rack::MockRequest helps testing your Rack application without
-
# actually using HTTP.
-
#
-
# After performing a request on a URL with get/post/put/patch/delete, it
-
# returns a MockResponse with useful helper methods for effective
-
# testing.
-
#
-
# You can pass a hash with additional configuration to the
-
# get/post/put/patch/delete.
-
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
-
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
-
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
-
-
1
class MockRequest
-
1
class FatalWarning < RuntimeError
-
end
-
-
1
class FatalWarner
-
1
def puts(warning)
-
raise FatalWarning, warning
-
end
-
-
1
def write(warning)
-
raise FatalWarning, warning
-
end
-
-
1
def flush
-
end
-
-
1
def string
-
""
-
end
-
end
-
-
DEFAULT_ENV = {
-
1
RACK_VERSION => Rack::VERSION,
-
RACK_INPUT => StringIO.new,
-
RACK_ERRORS => StringIO.new,
-
RACK_MULTITHREAD => true,
-
RACK_MULTIPROCESS => true,
-
RACK_RUNONCE => false,
-
}.freeze
-
-
1
def initialize(app)
-
@app = app
-
end
-
-
# Make a GET request and return a MockResponse. See #request.
-
1
def get(uri, opts = {}) request(GET, uri, opts) end
-
# Make a POST request and return a MockResponse. See #request.
-
1
def post(uri, opts = {}) request(POST, uri, opts) end
-
# Make a PUT request and return a MockResponse. See #request.
-
1
def put(uri, opts = {}) request(PUT, uri, opts) end
-
# Make a PATCH request and return a MockResponse. See #request.
-
1
def patch(uri, opts = {}) request(PATCH, uri, opts) end
-
# Make a DELETE request and return a MockResponse. See #request.
-
1
def delete(uri, opts = {}) request(DELETE, uri, opts) end
-
# Make a HEAD request and return a MockResponse. See #request.
-
1
def head(uri, opts = {}) request(HEAD, uri, opts) end
-
# Make an OPTIONS request and return a MockResponse. See #request.
-
1
def options(uri, opts = {}) request(OPTIONS, uri, opts) end
-
-
# Make a request using the given request method for the given
-
# uri to the rack application and return a MockResponse.
-
# Options given are passed to MockRequest.env_for.
-
1
def request(method = GET, uri = "", opts = {})
-
env = self.class.env_for(uri, opts.merge(method: method))
-
-
if opts[:lint]
-
app = Rack::Lint.new(@app)
-
else
-
app = @app
-
end
-
-
errors = env[RACK_ERRORS]
-
status, headers, body = app.call(env)
-
MockResponse.new(status, headers, body, errors)
-
ensure
-
body.close if body.respond_to?(:close)
-
end
-
-
# For historical reasons, we're pinning to RFC 2396.
-
# URI::Parser = URI::RFC2396_Parser
-
1
def self.parse_uri_rfc2396(uri)
-
1
@parser ||= URI::Parser.new
-
1
@parser.parse(uri)
-
end
-
-
# Return the Rack environment used for a request to +uri+.
-
# All options that are strings are added to the returned environment.
-
# Options:
-
# :fatal :: Whether to raise an exception if request outputs to rack.errors
-
# :input :: The rack.input to set
-
# :method :: The HTTP request method to use
-
# :params :: The params to use
-
# :script_name :: The SCRIPT_NAME to set
-
1
def self.env_for(uri = "", opts = {})
-
1
uri = parse_uri_rfc2396(uri)
-
1
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
-
-
1
env = DEFAULT_ENV.dup
-
-
1
env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
-
1
env[SERVER_NAME] = (uri.host || "example.org").b
-
1
env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
-
1
env[QUERY_STRING] = (uri.query.to_s).b
-
1
env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
-
1
env[RACK_URL_SCHEME] = (uri.scheme || "http").b
-
1
env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
-
-
1
env[SCRIPT_NAME] = opts[:script_name] || ""
-
-
1
if opts[:fatal]
-
env[RACK_ERRORS] = FatalWarner.new
-
else
-
1
env[RACK_ERRORS] = StringIO.new
-
end
-
-
1
if params = opts[:params]
-
if env[REQUEST_METHOD] == GET
-
params = Utils.parse_nested_query(params) if params.is_a?(String)
-
params.update(Utils.parse_nested_query(env[QUERY_STRING]))
-
env[QUERY_STRING] = Utils.build_nested_query(params)
-
elsif !opts.has_key?(:input)
-
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
-
if params.is_a?(Hash)
-
if data = Rack::Multipart.build_multipart(params)
-
opts[:input] = data
-
opts["CONTENT_LENGTH"] ||= data.length.to_s
-
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
-
else
-
opts[:input] = Utils.build_nested_query(params)
-
end
-
else
-
opts[:input] = params
-
end
-
end
-
end
-
-
1
empty_str = String.new
-
1
opts[:input] ||= empty_str
-
1
if String === opts[:input]
-
1
rack_input = StringIO.new(opts[:input])
-
else
-
rack_input = opts[:input]
-
end
-
-
1
rack_input.set_encoding(Encoding::BINARY)
-
1
env[RACK_INPUT] = rack_input
-
-
1
env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
-
-
1
opts.each { |field, value|
-
4
env[field] = value if String === field
-
}
-
-
1
env
-
end
-
end
-
-
# Rack::MockResponse provides useful helpers for testing your apps.
-
# Usually, you don't create the MockResponse on your own, but use
-
# MockRequest.
-
-
1
class MockResponse < Rack::Response
-
1
class << self
-
1
alias [] new
-
end
-
-
# Headers
-
1
attr_reader :original_headers, :cookies
-
-
# Errors
-
1
attr_accessor :errors
-
-
1
def initialize(status, headers, body, errors = StringIO.new(""))
-
@original_headers = headers
-
@errors = errors.string if errors.respond_to?(:string)
-
@cookies = parse_cookies_from_header
-
-
super(body, status, headers)
-
-
buffered_body!
-
end
-
-
1
def =~(other)
-
body =~ other
-
end
-
-
1
def match(other)
-
body.match other
-
end
-
-
1
def body
-
# FIXME: apparently users of MockResponse expect the return value of
-
# MockResponse#body to be a string. However, the real response object
-
# returns the body as a list.
-
#
-
# See spec_showstatus.rb:
-
#
-
# should "not replace existing messages" do
-
# ...
-
# res.body.should == "foo!"
-
# end
-
buffer = String.new
-
-
super.each do |chunk|
-
buffer << chunk
-
end
-
-
return buffer
-
end
-
-
1
def empty?
-
[201, 204, 304].include? status
-
end
-
-
1
def cookie(name)
-
cookies.fetch(name, nil)
-
end
-
-
1
private
-
-
1
def parse_cookies_from_header
-
cookies = Hash.new
-
if original_headers.has_key? 'Set-Cookie'
-
set_cookie_header = original_headers.fetch('Set-Cookie')
-
set_cookie_header.split("\n").each do |cookie|
-
cookie_name, cookie_filling = cookie.split('=', 2)
-
cookie_attributes = identify_cookie_attributes cookie_filling
-
parsed_cookie = CGI::Cookie.new(
-
'name' => cookie_name.strip,
-
'value' => cookie_attributes.fetch('value'),
-
'path' => cookie_attributes.fetch('path', nil),
-
'domain' => cookie_attributes.fetch('domain', nil),
-
'expires' => cookie_attributes.fetch('expires', nil),
-
'secure' => cookie_attributes.fetch('secure', false)
-
)
-
cookies.store(cookie_name, parsed_cookie)
-
end
-
end
-
cookies
-
end
-
-
1
def identify_cookie_attributes(cookie_filling)
-
cookie_bits = cookie_filling.split(';')
-
cookie_attributes = Hash.new
-
cookie_attributes.store('value', cookie_bits[0].strip)
-
cookie_bits.each do |bit|
-
if bit.include? '='
-
cookie_attribute, attribute_value = bit.split('=')
-
cookie_attributes.store(cookie_attribute.strip, attribute_value.strip)
-
if cookie_attribute.include? 'max-age'
-
cookie_attributes.store('expires', Time.now + attribute_value.strip.to_i)
-
end
-
end
-
if bit.include? 'secure'
-
cookie_attributes.store('secure', true)
-
end
-
end
-
cookie_attributes
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'time'
-
-
1
module Rack
-
# Rack::Response provides a convenient interface to create a Rack
-
# response.
-
#
-
# It allows setting of headers and cookies, and provides useful
-
# defaults (an OK response with empty headers and body).
-
#
-
# You can use Response#write to iteratively generate your response,
-
# but note that this is buffered by Rack::Response until you call
-
# +finish+. +finish+ however can take a block inside which calls to
-
# +write+ are synchronous with the Rack response.
-
#
-
# Your application's +call+ should end returning Response#finish.
-
1
class Response
-
1
def self.[](status, headers, body)
-
self.new(body, status, headers)
-
end
-
-
1
CHUNKED = 'chunked'
-
1
STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
-
-
1
attr_accessor :length, :status, :body
-
1
attr_reader :headers
-
-
# @deprecated Use {#headers} instead.
-
1
alias header headers
-
-
# Initialize the response object with the specified body, status
-
# and headers.
-
#
-
# @param body [nil, #each, #to_str] the response body.
-
# @param status [Integer] the integer status as defined by the
-
# HTTP protocol RFCs.
-
# @param headers [#each] a list of key-value header pairs which
-
# conform to the HTTP protocol RFCs.
-
#
-
# Providing a body which responds to #to_str is legacy behaviour.
-
1
def initialize(body = nil, status = 200, headers = {})
-
@status = status.to_i
-
@headers = Utils::HeaderHash[headers]
-
-
@writer = self.method(:append)
-
-
@block = nil
-
-
# Keep track of whether we have expanded the user supplied body.
-
if body.nil?
-
@body = []
-
@buffered = true
-
@length = 0
-
elsif body.respond_to?(:to_str)
-
@body = [body]
-
@buffered = true
-
@length = body.to_str.bytesize
-
else
-
@body = body
-
@buffered = false
-
@length = 0
-
end
-
-
yield self if block_given?
-
end
-
-
1
def redirect(target, status = 302)
-
self.status = status
-
self.location = target
-
end
-
-
1
def chunked?
-
CHUNKED == get_header(TRANSFER_ENCODING)
-
end
-
-
# Generate a response array consistent with the requirements of the SPEC.
-
# @return [Array] a 3-tuple suitable of `[status, headers, body]`
-
# which is suitable to be returned from the middleware `#call(env)` method.
-
1
def finish(&block)
-
if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
-
delete_header CONTENT_TYPE
-
delete_header CONTENT_LENGTH
-
close
-
return [@status, @headers, []]
-
else
-
if block_given?
-
@block = block
-
return [@status, @headers, self]
-
else
-
return [@status, @headers, @body]
-
end
-
end
-
end
-
-
1
alias to_a finish # For *response
-
-
1
def each(&callback)
-
@body.each(&callback)
-
@buffered = true
-
-
if @block
-
@writer = callback
-
@block.call(self)
-
end
-
end
-
-
# Append to body and update Content-Length.
-
#
-
# NOTE: Do not mix #write and direct #body access!
-
#
-
1
def write(chunk)
-
buffered_body!
-
-
@writer.call(chunk.to_s)
-
end
-
-
1
def close
-
@body.close if @body.respond_to?(:close)
-
end
-
-
1
def empty?
-
@block == nil && @body.empty?
-
end
-
-
1
def has_header?(key); headers.key? key; end
-
1
def get_header(key); headers[key]; end
-
1
def set_header(key, v); headers[key] = v; end
-
1
def delete_header(key); headers.delete key; end
-
-
1
alias :[] :get_header
-
1
alias :[]= :set_header
-
-
1
module Helpers
-
1
def invalid?; status < 100 || status >= 600; end
-
-
1
def informational?; status >= 100 && status < 200; end
-
1
def successful?; status >= 200 && status < 300; end
-
1
def redirection?; status >= 300 && status < 400; end
-
1
def client_error?; status >= 400 && status < 500; end
-
1
def server_error?; status >= 500 && status < 600; end
-
-
1
def ok?; status == 200; end
-
1
def created?; status == 201; end
-
1
def accepted?; status == 202; end
-
1
def no_content?; status == 204; end
-
1
def moved_permanently?; status == 301; end
-
1
def bad_request?; status == 400; end
-
1
def unauthorized?; status == 401; end
-
1
def forbidden?; status == 403; end
-
1
def not_found?; status == 404; end
-
1
def method_not_allowed?; status == 405; end
-
1
def precondition_failed?; status == 412; end
-
1
def unprocessable?; status == 422; end
-
-
1
def redirect?; [301, 302, 303, 307, 308].include? status; end
-
-
1
def include?(header)
-
has_header? header
-
end
-
-
# Add a header that may have multiple values.
-
#
-
# Example:
-
# response.add_header 'Vary', 'Accept-Encoding'
-
# response.add_header 'Vary', 'Cookie'
-
#
-
# assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
-
#
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
-
1
def add_header(key, v)
-
if v.nil?
-
get_header key
-
elsif has_header? key
-
set_header key, "#{get_header key},#{v}"
-
else
-
set_header key, v
-
end
-
end
-
-
# Get the content type of the response.
-
1
def content_type
-
get_header CONTENT_TYPE
-
end
-
-
# Set the content type of the response.
-
1
def content_type=(content_type)
-
set_header CONTENT_TYPE, content_type
-
end
-
-
1
def media_type
-
MediaType.type(content_type)
-
end
-
-
1
def media_type_params
-
MediaType.params(content_type)
-
end
-
-
1
def content_length
-
cl = get_header CONTENT_LENGTH
-
cl ? cl.to_i : cl
-
end
-
-
1
def location
-
get_header "Location"
-
end
-
-
1
def location=(location)
-
set_header "Location", location
-
end
-
-
1
def set_cookie(key, value)
-
cookie_header = get_header SET_COOKIE
-
set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
-
end
-
-
1
def delete_cookie(key, value = {})
-
set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
-
end
-
-
1
def set_cookie_header
-
get_header SET_COOKIE
-
end
-
-
1
def set_cookie_header=(v)
-
set_header SET_COOKIE, v
-
end
-
-
1
def cache_control
-
get_header CACHE_CONTROL
-
end
-
-
1
def cache_control=(v)
-
set_header CACHE_CONTROL, v
-
end
-
-
# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
-
1
def do_not_cache!
-
set_header CACHE_CONTROL, "no-cache, must-revalidate"
-
set_header EXPIRES, Time.now.httpdate
-
end
-
-
# Specify that the content should be cached.
-
# @param duration [Integer] The number of seconds until the cache expires.
-
# @option directive [String] The cache control directive, one of "public", "private", "no-cache" or "no-store".
-
1
def cache!(duration = 3600, directive: "public")
-
unless headers[CACHE_CONTROL] =~ /no-cache/
-
set_header CACHE_CONTROL, "#{directive}, max-age=#{duration}"
-
set_header EXPIRES, (Time.now + duration).httpdate
-
end
-
end
-
-
1
def etag
-
get_header ETAG
-
end
-
-
1
def etag=(v)
-
set_header ETAG, v
-
end
-
-
1
protected
-
-
1
def buffered_body!
-
return if @buffered
-
-
if @body.is_a?(Array)
-
# The user supplied body was an array:
-
@body = @body.compact
-
@body.each do |part|
-
@length += part.to_s.bytesize
-
end
-
else
-
# Turn the user supplied body into a buffered array:
-
body = @body
-
@body = Array.new
-
-
body.each do |part|
-
@writer.call(part.to_s)
-
end
-
-
body.close if body.respond_to?(:close)
-
end
-
-
@buffered = true
-
end
-
-
1
def append(chunk)
-
@body << chunk
-
-
unless chunked?
-
@length += chunk.bytesize
-
set_header(CONTENT_LENGTH, @length.to_s)
-
end
-
-
return chunk
-
end
-
end
-
-
1
include Helpers
-
-
1
class Raw
-
1
include Helpers
-
-
1
attr_reader :headers
-
1
attr_accessor :status
-
-
1
def initialize(status, headers)
-
@status = status
-
@headers = headers
-
end
-
-
1
def has_header?(key); headers.key? key; end
-
1
def get_header(key); headers[key]; end
-
1
def set_header(key, v); headers[key] = v; end
-
1
def delete_header(key); headers.delete key; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
-
# bugrep: Andreas Zehnder
-
-
1
require_relative '../../../rack'
-
1
require 'time'
-
1
require 'securerandom'
-
1
require 'digest/sha2'
-
-
1
module Rack
-
-
1
module Session
-
-
1
class SessionId
-
1
ID_VERSION = 2
-
-
1
attr_reader :public_id
-
-
1
def initialize(public_id)
-
@public_id = public_id
-
end
-
-
1
def private_id
-
"#{ID_VERSION}::#{hash_sid(public_id)}"
-
end
-
-
1
alias :cookie_value :public_id
-
1
alias :to_s :public_id
-
-
1
def empty?; false; end
-
1
def inspect; public_id.inspect; end
-
-
1
private
-
-
1
def hash_sid(sid)
-
Digest::SHA256.hexdigest(sid)
-
end
-
end
-
-
1
module Abstract
-
# SessionHash is responsible to lazily load the session from store.
-
-
1
class SessionHash
-
1
include Enumerable
-
1
attr_writer :id
-
-
1
Unspecified = Object.new
-
-
1
def self.find(req)
-
req.get_header RACK_SESSION
-
end
-
-
1
def self.set(req, session)
-
req.set_header RACK_SESSION, session
-
end
-
-
1
def self.set_options(req, options)
-
req.set_header RACK_SESSION_OPTIONS, options.dup
-
end
-
-
1
def initialize(store, req)
-
@store = store
-
@req = req
-
@loaded = false
-
end
-
-
1
def id
-
return @id if @loaded or instance_variable_defined?(:@id)
-
@id = @store.send(:extract_session_id, @req)
-
end
-
-
1
def options
-
@req.session_options
-
end
-
-
1
def each(&block)
-
load_for_read!
-
@data.each(&block)
-
end
-
-
1
def [](key)
-
load_for_read!
-
@data[key.to_s]
-
end
-
-
1
def dig(key, *keys)
-
load_for_read!
-
@data.dig(key.to_s, *keys)
-
end
-
-
1
def fetch(key, default = Unspecified, &block)
-
load_for_read!
-
if default == Unspecified
-
@data.fetch(key.to_s, &block)
-
else
-
@data.fetch(key.to_s, default, &block)
-
end
-
end
-
-
1
def has_key?(key)
-
load_for_read!
-
@data.has_key?(key.to_s)
-
end
-
1
alias :key? :has_key?
-
1
alias :include? :has_key?
-
-
1
def []=(key, value)
-
load_for_write!
-
@data[key.to_s] = value
-
end
-
1
alias :store :[]=
-
-
1
def clear
-
load_for_write!
-
@data.clear
-
end
-
-
1
def destroy
-
clear
-
@id = @store.send(:delete_session, @req, id, options)
-
end
-
-
1
def to_hash
-
load_for_read!
-
@data.dup
-
end
-
-
1
def update(hash)
-
load_for_write!
-
@data.update(stringify_keys(hash))
-
end
-
1
alias :merge! :update
-
-
1
def replace(hash)
-
load_for_write!
-
@data.replace(stringify_keys(hash))
-
end
-
-
1
def delete(key)
-
load_for_write!
-
@data.delete(key.to_s)
-
end
-
-
1
def inspect
-
if loaded?
-
@data.inspect
-
else
-
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
-
end
-
end
-
-
1
def exists?
-
return @exists if instance_variable_defined?(:@exists)
-
@data = {}
-
@exists = @store.send(:session_exists?, @req)
-
end
-
-
1
def loaded?
-
@loaded
-
end
-
-
1
def empty?
-
load_for_read!
-
@data.empty?
-
end
-
-
1
def keys
-
load_for_read!
-
@data.keys
-
end
-
-
1
def values
-
load_for_read!
-
@data.values
-
end
-
-
1
private
-
-
1
def load_for_read!
-
load! if !loaded? && exists?
-
end
-
-
1
def load_for_write!
-
load! unless loaded?
-
end
-
-
1
def load!
-
@id, session = @store.send(:load_session, @req)
-
@data = stringify_keys(session)
-
@loaded = true
-
end
-
-
1
def stringify_keys(other)
-
# Use transform_keys after dropping Ruby 2.4 support
-
hash = {}
-
other.to_hash.each do |key, value|
-
hash[key.to_s] = value
-
end
-
hash
-
end
-
end
-
-
# ID sets up a basic framework for implementing an id based sessioning
-
# service. Cookies sent to the client for maintaining sessions will only
-
# contain an id reference. Only #find_session, #write_session and
-
# #delete_session are required to be overwritten.
-
#
-
# All parameters are optional.
-
# * :key determines the name of the cookie, by default it is
-
# 'rack.session'
-
# * :path, :domain, :expire_after, :secure, and :httponly set the related
-
# cookie options as by Rack::Response#set_cookie
-
# * :skip will not a set a cookie in the response nor update the session state
-
# * :defer will not set a cookie in the response but still update the session
-
# state if it is used with a backend
-
# * :renew (implementation dependent) will prompt the generation of a new
-
# session id, and migration of data to be referenced at the new id. If
-
# :defer is set, it will be overridden and the cookie will be set.
-
# * :sidbits sets the number of bits in length that a generated session
-
# id will be.
-
#
-
# These options can be set on a per request basis, at the location of
-
# <tt>env['rack.session.options']</tt>. Additionally the id of the
-
# session can be found within the options hash at the key :id. It is
-
# highly not recommended to change its value.
-
#
-
# Is Rack::Utils::Context compatible.
-
#
-
# Not included by default; you must require 'rack/session/abstract/id'
-
# to use.
-
-
1
class Persisted
-
DEFAULT_OPTIONS = {
-
1
key: RACK_SESSION,
-
path: '/',
-
domain: nil,
-
expire_after: nil,
-
secure: false,
-
httponly: true,
-
defer: false,
-
renew: false,
-
sidbits: 128,
-
cookie_only: true,
-
secure_random: ::SecureRandom
-
}.freeze
-
-
1
attr_reader :key, :default_options, :sid_secure
-
-
1
def initialize(app, options = {})
-
@app = app
-
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
-
@key = @default_options.delete(:key)
-
@cookie_only = @default_options.delete(:cookie_only)
-
@same_site = @default_options.delete(:same_site)
-
initialize_sid
-
end
-
-
1
def call(env)
-
context(env)
-
end
-
-
1
def context(env, app = @app)
-
req = make_request env
-
prepare_session(req)
-
status, headers, body = app.call(req.env)
-
res = Rack::Response::Raw.new status, headers
-
commit_session(req, res)
-
[status, headers, body]
-
end
-
-
1
private
-
-
1
def make_request(env)
-
Rack::Request.new env
-
end
-
-
1
def initialize_sid
-
@sidbits = @default_options[:sidbits]
-
@sid_secure = @default_options[:secure_random]
-
@sid_length = @sidbits / 4
-
end
-
-
# Generate a new session id using Ruby #rand. The size of the
-
# session id is controlled by the :sidbits option.
-
# Monkey patch this to use custom methods for session id generation.
-
-
1
def generate_sid(secure = @sid_secure)
-
if secure
-
secure.hex(@sid_length)
-
else
-
"%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
-
end
-
rescue NotImplementedError
-
generate_sid(false)
-
end
-
-
# Sets the lazy session at 'rack.session' and places options and session
-
# metadata into 'rack.session.options'.
-
-
1
def prepare_session(req)
-
session_was = req.get_header RACK_SESSION
-
session = session_class.new(self, req)
-
req.set_header RACK_SESSION, session
-
req.set_header RACK_SESSION_OPTIONS, @default_options.dup
-
session.merge! session_was if session_was
-
end
-
-
# Extracts the session id from provided cookies and passes it and the
-
# environment to #find_session.
-
-
1
def load_session(req)
-
sid = current_session_id(req)
-
sid, session = find_session(req, sid)
-
[sid, session || {}]
-
end
-
-
# Extract session id from request object.
-
-
1
def extract_session_id(request)
-
sid = request.cookies[@key]
-
sid ||= request.params[@key] unless @cookie_only
-
sid
-
end
-
-
# Returns the current session id from the SessionHash.
-
-
1
def current_session_id(req)
-
req.get_header(RACK_SESSION).id
-
end
-
-
# Check if the session exists or not.
-
-
1
def session_exists?(req)
-
value = current_session_id(req)
-
value && !value.empty?
-
end
-
-
# Session should be committed if it was loaded, any of specific options like :renew, :drop
-
# or :expire_after was given and the security permissions match. Skips if skip is given.
-
-
1
def commit_session?(req, session, options)
-
if options[:skip]
-
false
-
else
-
has_session = loaded_session?(session) || forced_session_update?(session, options)
-
has_session && security_matches?(req, options)
-
end
-
end
-
-
1
def loaded_session?(session)
-
!session.is_a?(session_class) || session.loaded?
-
end
-
-
1
def forced_session_update?(session, options)
-
force_options?(options) && session && !session.empty?
-
end
-
-
1
def force_options?(options)
-
options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
-
end
-
-
1
def security_matches?(request, options)
-
return true unless options[:secure]
-
request.ssl?
-
end
-
-
# Acquires the session from the environment and the session id from
-
# the session options and passes them to #write_session. If successful
-
# and the :defer option is not true, a cookie will be added to the
-
# response with the session's id.
-
-
1
def commit_session(req, res)
-
session = req.get_header RACK_SESSION
-
options = session.options
-
-
if options[:drop] || options[:renew]
-
session_id = delete_session(req, session.id || generate_sid, options)
-
return unless session_id
-
end
-
-
return unless commit_session?(req, session, options)
-
-
session.send(:load!) unless loaded_session?(session)
-
session_id ||= session.id
-
session_data = session.to_hash.delete_if { |k, v| v.nil? }
-
-
if not data = write_session(req, session_id, session_data, options)
-
req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
-
elsif options[:defer] and not options[:renew]
-
req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
-
else
-
cookie = Hash.new
-
cookie[:value] = cookie_value(data)
-
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
-
cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
-
-
if @same_site.respond_to? :call
-
cookie[:same_site] = @same_site.call(req, res)
-
else
-
cookie[:same_site] = @same_site
-
end
-
set_cookie(req, res, cookie.merge!(options))
-
end
-
end
-
1
public :commit_session
-
-
1
def cookie_value(data)
-
data
-
end
-
-
# Sets the cookie back to the client with session id. We skip the cookie
-
# setting if the value didn't change (sid is the same) or expires was given.
-
-
1
def set_cookie(request, res, cookie)
-
if request.cookies[@key] != cookie[:value] || cookie[:expires]
-
res.set_cookie_header =
-
Utils.add_cookie_to_header(res.set_cookie_header, @key, cookie)
-
end
-
end
-
-
# Allow subclasses to prepare_session for different Session classes
-
-
1
def session_class
-
SessionHash
-
end
-
-
# All thread safety and session retrieval procedures should occur here.
-
# Should return [session_id, session].
-
# If nil is provided as the session id, generation of a new valid id
-
# should occur within.
-
-
1
def find_session(env, sid)
-
raise '#find_session not implemented.'
-
end
-
-
# All thread safety and session storage procedures should occur here.
-
# Must return the session id if the session was saved successfully, or
-
# false if the session could not be saved.
-
-
1
def write_session(req, sid, session, options)
-
raise '#write_session not implemented.'
-
end
-
-
# All thread safety and session destroy procedures should occur here.
-
# Should return a new session id or nil if options[:drop]
-
-
1
def delete_session(req, sid, options)
-
raise '#delete_session not implemented'
-
end
-
end
-
-
1
class PersistedSecure < Persisted
-
1
class SecureSessionHash < SessionHash
-
1
def [](key)
-
if key == "session_id"
-
load_for_read!
-
id.public_id if id
-
else
-
super
-
end
-
end
-
end
-
-
1
def generate_sid(*)
-
public_id = super
-
-
SessionId.new(public_id)
-
end
-
-
1
def extract_session_id(*)
-
public_id = super
-
public_id && SessionId.new(public_id)
-
end
-
-
1
private
-
-
1
def session_class
-
SecureSessionHash
-
end
-
-
1
def cookie_value(data)
-
data.cookie_value
-
end
-
end
-
-
1
class ID < Persisted
-
1
def self.inherited(klass)
-
k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
-
unless k.instance_variable_defined?(:"@_rack_warned")
-
warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
-
k.instance_variable_set(:"@_rack_warned", true)
-
end
-
super
-
end
-
-
# All thread safety and session retrieval procedures should occur here.
-
# Should return [session_id, session].
-
# If nil is provided as the session id, generation of a new valid id
-
# should occur within.
-
-
1
def find_session(req, sid)
-
get_session req.env, sid
-
end
-
-
# All thread safety and session storage procedures should occur here.
-
# Must return the session id if the session was saved successfully, or
-
# false if the session could not be saved.
-
-
1
def write_session(req, sid, session, options)
-
set_session req.env, sid, session, options
-
end
-
-
# All thread safety and session destroy procedures should occur here.
-
# Should return a new session id or nil if options[:drop]
-
-
1
def delete_session(req, sid, options)
-
destroy_session req.env, sid, options
-
end
-
end
-
end
-
end
-
end
-
1
require 'rails/dom/testing/assertions'
-
1
require 'active_support/concern'
-
1
require 'nokogiri'
-
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
1
autoload :DomAssertions, 'rails/dom/testing/assertions/dom_assertions'
-
1
autoload :SelectorAssertions, 'rails/dom/testing/assertions/selector_assertions'
-
-
1
extend ActiveSupport::Concern
-
-
1
include DomAssertions
-
1
include SelectorAssertions
-
end
-
end
-
end
-
end
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
1
module DomAssertions
-
# \Test two HTML strings for equivalency (e.g., equal even when attributes are in another order)
-
#
-
# # assert that the referenced method generates the appropriate HTML string
-
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
-
1
def assert_dom_equal(expected, actual, message = nil)
-
expected_dom, actual_dom = fragment(expected), fragment(actual)
-
message ||= "Expected: #{expected}\nActual: #{actual}"
-
assert compare_doms(expected_dom, actual_dom), message
-
end
-
-
# The negated form of +assert_dom_equal+.
-
#
-
# # assert that the referenced method does not generate the specified HTML string
-
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
-
1
def assert_dom_not_equal(expected, actual, message = nil)
-
expected_dom, actual_dom = fragment(expected), fragment(actual)
-
message ||= "Expected: #{expected}\nActual: #{actual}"
-
assert_not compare_doms(expected_dom, actual_dom), message
-
end
-
-
1
protected
-
-
1
def compare_doms(expected, actual)
-
return false unless expected.children.size == actual.children.size
-
-
expected.children.each_with_index do |child, i|
-
return false unless equal_children?(child, actual.children[i])
-
end
-
-
true
-
end
-
-
1
def equal_children?(child, other_child)
-
return false unless child.type == other_child.type
-
-
if child.element?
-
child.name == other_child.name &&
-
equal_attribute_nodes?(child.attribute_nodes, other_child.attribute_nodes) &&
-
compare_doms(child, other_child)
-
else
-
child.to_s == other_child.to_s
-
end
-
end
-
-
1
def equal_attribute_nodes?(nodes, other_nodes)
-
return false unless nodes.size == other_nodes.size
-
-
nodes = nodes.sort_by(&:name)
-
other_nodes = other_nodes.sort_by(&:name)
-
-
nodes.each_with_index do |attr, i|
-
return false unless equal_attribute?(attr, other_nodes[i])
-
end
-
-
true
-
end
-
-
1
def equal_attribute?(attr, other_attr)
-
attr.name == other_attr.name && attr.value == other_attr.value
-
end
-
-
1
private
-
-
1
def fragment(text)
-
Nokogiri::HTML::DocumentFragment.parse(text)
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'active_support/deprecation'
-
1
require_relative 'selector_assertions/count_describable'
-
1
require_relative 'selector_assertions/html_selector'
-
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
# Adds the +assert_select+ method for use in Rails functional
-
# test cases, which can be used to make assertions on the response HTML of a controller
-
# action. You can also call +assert_select+ within another +assert_select+ to
-
# make assertions on elements selected by the enclosing assertion.
-
#
-
# Use +css_select+ to select elements without making an assertions, either
-
# from the response HTML or elements selected by the enclosing assertion.
-
#
-
# In addition to HTML responses, you can make the following assertions:
-
#
-
# * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
-
# * +assert_select_email+ - Assertions on the HTML body of an e-mail.
-
1
module SelectorAssertions
-
-
# Select and return all matching elements.
-
#
-
# If called with a single argument, uses that argument as a selector.
-
# Called without an element +css_select+ selects from
-
# the element returned in +document_root_element+
-
#
-
# The default implementation of +document_root_element+ raises an exception explaining this.
-
#
-
# Returns an empty Nokogiri::XML::NodeSet if no match is found.
-
#
-
# If called with two arguments, uses the first argument as the root
-
# element and the second argument as the selector. Attempts to match the
-
# root element and any of its children.
-
# Returns an empty Nokogiri::XML::NodeSet if no match is found.
-
#
-
# The selector may be a CSS selector expression (String).
-
# css_select returns nil if called with an invalid css selector.
-
#
-
# # Selects all div tags
-
# divs = css_select("div")
-
#
-
# # Selects all paragraph tags and does something interesting
-
# pars = css_select("p")
-
# pars.each do |par|
-
# # Do something fun with paragraphs here...
-
# end
-
#
-
# # Selects all list items in unordered lists
-
# items = css_select("ul>li")
-
#
-
# # Selects all form tags and then all inputs inside the form
-
# forms = css_select("form")
-
# forms.each do |form|
-
# inputs = css_select(form, "input")
-
# ...
-
# end
-
1
def css_select(*args)
-
raise ArgumentError, "you at least need a selector argument" if args.empty?
-
-
root = args.size == 1 ? document_root_element : args.shift
-
-
nodeset(root).css(args.first)
-
end
-
-
# An assertion that selects elements and makes one or more equality tests.
-
#
-
# If the first argument is an element, selects all matching elements
-
# starting from (and including) that element and all its children in
-
# depth-first order.
-
#
-
# If no element is specified +assert_select+ selects from
-
# the element returned in +document_root_element+
-
# unless +assert_select+ is called from within an +assert_select+ block.
-
# Override +document_root_element+ to tell +assert_select+ what to select from.
-
# The default implementation raises an exception explaining this.
-
#
-
# When called with a block +assert_select+ passes an array of selected elements
-
# to the block. Calling +assert_select+ from the block, with no element specified,
-
# runs the assertion on the complete set of elements selected by the enclosing assertion.
-
# Alternatively the array may be iterated through so that +assert_select+ can be called
-
# separately for each element.
-
#
-
#
-
# ==== Example
-
# If the response contains two ordered lists, each with four list elements then:
-
# assert_select "ol" do |elements|
-
# elements.each do |element|
-
# assert_select element, "li", 4
-
# end
-
# end
-
#
-
# will pass, as will:
-
# assert_select "ol" do
-
# assert_select "li", 8
-
# end
-
#
-
# The selector may be a CSS selector expression (String) or an expression
-
# with substitution values (Array).
-
# Substitution uses a custom pseudo class match. Pass in whatever attribute you want to match (enclosed in quotes) and a ? for the substitution.
-
# assert_select returns nil if called with an invalid css selector.
-
#
-
# assert_select "div:match('id', ?)", /\d+/
-
#
-
# === Equality Tests
-
#
-
# The equality test may be one of the following:
-
# * <tt>true</tt> - Assertion is true if at least one element selected.
-
# * <tt>false</tt> - Assertion is true if no element selected.
-
# * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
-
# one element matches the string or regular expression.
-
# * <tt>Integer</tt> - Assertion is true if exactly that number of
-
# elements are selected.
-
# * <tt>Range</tt> - Assertion is true if the number of selected
-
# elements fit the range.
-
# If no equality test specified, the assertion is true if at least one
-
# element selected.
-
#
-
# To perform more than one equality tests, use a hash with the following keys:
-
# * <tt>:text</tt> - Narrow the selection to elements that have this text
-
# value (string or regexp).
-
# * <tt>:html</tt> - Narrow the selection to elements that have this HTML
-
# content (string or regexp).
-
# * <tt>:count</tt> - Assertion is true if the number of selected elements
-
# is equal to this value.
-
# * <tt>:minimum</tt> - Assertion is true if the number of selected
-
# elements is at least this value.
-
# * <tt>:maximum</tt> - Assertion is true if the number of selected
-
# elements is at most this value.
-
#
-
# If the method is called with a block, once all equality tests are
-
# evaluated the block is called with an array of all matched elements.
-
#
-
# # At least one form element
-
# assert_select "form"
-
#
-
# # Form element includes four input fields
-
# assert_select "form input", 4
-
#
-
# # Page title is "Welcome"
-
# assert_select "title", "Welcome"
-
#
-
# # Page title is "Welcome" and there is only one title element
-
# assert_select "title", {count: 1, text: "Welcome"},
-
# "Wrong title or more than one title element"
-
#
-
# # Page contains no forms
-
# assert_select "form", false, "This page must contain no forms"
-
#
-
# # Test the content and style
-
# assert_select "body div.header ul.menu"
-
#
-
# # Use substitution values
-
# assert_select "ol>li:match('id', ?)", /item-\d+/
-
#
-
# # All input fields in the form have a name
-
# assert_select "form input" do
-
# assert_select ":match('name', ?)", /.+/ # Not empty
-
# end
-
1
def assert_select(*args, &block)
-
@selected ||= nil
-
-
selector = HTMLSelector.new(args, @selected) { nodeset document_root_element }
-
-
if selector.selecting_no_body?
-
assert true
-
return
-
end
-
-
selector.select.tap do |matches|
-
assert_size_match!(matches.size, selector.tests,
-
selector.css_selector, selector.message)
-
-
nest_selection(matches, &block) if block_given? && !matches.empty?
-
end
-
end
-
-
# Extracts the content of an element, treats it as encoded HTML and runs
-
# nested assertion on it.
-
#
-
# You typically call this method within another assertion to operate on
-
# all currently selected elements. You can also pass an element or array
-
# of elements.
-
#
-
# The content of each element is un-encoded, and wrapped in the root
-
# element +encoded+. It then calls the block with all un-encoded elements.
-
#
-
# # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
-
# assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
-
# # Select each entry item and then the title item
-
# assert_select "entry>title" do
-
# # Run assertions on the encoded title elements
-
# assert_select_encoded do
-
# assert_select "b"
-
# end
-
# end
-
# end
-
#
-
#
-
# # Selects all paragraph tags from within the description of an RSS feed
-
# assert_select "rss[version=2.0]" do
-
# # Select description element of each feed item.
-
# assert_select "channel>item>description" do
-
# # Run assertions on the encoded elements.
-
# assert_select_encoded do
-
# assert_select "p"
-
# end
-
# end
-
# end
-
1
def assert_select_encoded(element = nil, &block)
-
if !element && !@selected
-
raise ArgumentError, "Element is required when called from a nonnested assert_select"
-
end
-
-
content = nodeset(element || @selected).map do |elem|
-
elem.children.select do |child|
-
child.cdata? || (child.text? && !child.blank?)
-
end.map(&:content)
-
end.join
-
-
selected = Nokogiri::HTML::DocumentFragment.parse(content)
-
nest_selection(selected) do
-
if content.empty?
-
yield selected
-
else
-
assert_select ":root", &block
-
end
-
end
-
end
-
-
# Extracts the body of an email and runs nested assertions on it.
-
#
-
# You must enable deliveries for this assertion to work, use:
-
# ActionMailer::Base.perform_deliveries = true
-
#
-
# assert_select_email do
-
# assert_select "h1", "Email alert"
-
# end
-
#
-
# assert_select_email do
-
# items = assert_select "ol>li"
-
# items.each do
-
# # Work with items here...
-
# end
-
# end
-
1
def assert_select_email(&block)
-
deliveries = ActionMailer::Base.deliveries
-
assert !deliveries.empty?, "No e-mail in delivery list"
-
-
deliveries.each do |delivery|
-
(delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
-
if part["Content-Type"].to_s =~ /^text\/html\W/
-
root = Nokogiri::HTML::DocumentFragment.parse(part.body.to_s)
-
assert_select root, ":root", &block
-
end
-
end
-
end
-
end
-
-
1
private
-
1
include CountDescribable
-
-
1
def document_root_element
-
raise NotImplementedError, 'Implementing document_root_element makes ' \
-
'assert_select work without needing to specify an element to select from.'
-
end
-
-
# +equals+ must contain :minimum, :maximum and :count keys
-
1
def assert_size_match!(size, equals, css_selector, message = nil)
-
min, max, count = equals[:minimum], equals[:maximum], equals[:count]
-
-
message ||= %(Expected #{count_description(min, max, count)} matching "#{css_selector}", found #{size}.)
-
if count
-
assert_equal count, size, message
-
else
-
assert_operator size, :>=, min, message if min
-
assert_operator size, :<=, max, message if max
-
end
-
end
-
-
1
def nest_selection(selection)
-
# Set @selected to allow nested assert_select.
-
# Can be nested several levels deep.
-
old_selected, @selected = @selected, selection
-
yield @selected
-
ensure
-
@selected = old_selected
-
end
-
-
1
def nodeset(node)
-
if node.is_a?(Nokogiri::XML::NodeSet)
-
node
-
else
-
Nokogiri::XML::NodeSet.new(node.document, [node])
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'active_support/concern'
-
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
1
module SelectorAssertions
-
1
module CountDescribable
-
1
extend ActiveSupport::Concern
-
-
1
private
-
1
def count_description(min, max, count) #:nodoc:
-
if min && max && (max != min)
-
"between #{min} and #{max} elements"
-
elsif min && max && max == min && count
-
"exactly #{count} #{pluralize_element(min)}"
-
elsif min && !(min == 1 && max == 1)
-
"at least #{min} #{pluralize_element(min)}"
-
elsif max
-
"at most #{max} #{pluralize_element(max)}"
-
end
-
end
-
-
1
def pluralize_element(quantity)
-
quantity == 1 ? 'element' : 'elements'
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'active_support/core_ext/module/attribute_accessors'
-
1
require_relative 'substitution_context'
-
-
1
class HTMLSelector #:nodoc:
-
1
attr_reader :css_selector, :tests, :message
-
-
1
def initialize(values, previous_selection = nil, &root_fallback)
-
@values = values
-
@root = extract_root(previous_selection, root_fallback)
-
extract_selectors
-
@tests = extract_equality_tests
-
@message = @values.shift
-
-
if @values.shift
-
raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
-
end
-
end
-
-
1
def selecting_no_body? #:nodoc:
-
# Nokogiri gives the document a body element. Which means we can't
-
# run an assertion expecting there to not be a body.
-
@selector == 'body' && @tests[:count] == 0
-
end
-
-
1
def select
-
filter @root.css(@selector, context)
-
end
-
-
1
private
-
-
1
NO_STRIP = %w{pre script style textarea}
-
-
2
mattr_reader(:context) { SubstitutionContext.new }
-
-
1
def filter(matches)
-
match_with = tests[:text] || tests[:html]
-
return matches if matches.empty? || !match_with
-
-
content_mismatch = nil
-
text_matches = tests.has_key?(:text)
-
regex_matching = match_with.is_a?(Regexp)
-
-
remaining = matches.reject do |match|
-
# Preserve markup with to_s for html elements
-
content = text_matches ? match.text : match.children.to_s
-
-
content.strip! unless NO_STRIP.include?(match.name)
-
content.sub!(/\A\n/, '') if text_matches && match.name == "textarea"
-
-
next if regex_matching ? (content =~ match_with) : (content == match_with)
-
content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, content)
-
true
-
end
-
-
@message ||= content_mismatch if remaining.empty?
-
Nokogiri::XML::NodeSet.new(matches.document, remaining)
-
end
-
-
1
def extract_root(previous_selection, root_fallback)
-
possible_root = @values.first
-
-
if possible_root == nil
-
raise ArgumentError, 'First argument is either selector or element ' \
-
'to select, but nil found. Perhaps you called assert_select with ' \
-
'an element that does not exist?'
-
elsif possible_root.respond_to?(:css)
-
@values.shift # remove the root, so selector is the first argument
-
possible_root
-
elsif previous_selection
-
previous_selection
-
else
-
root_fallback.call
-
end
-
end
-
-
1
def extract_selectors
-
selector = @values.shift
-
-
unless selector.is_a? String
-
raise ArgumentError, "Expecting a selector as the first argument"
-
end
-
-
@css_selector = context.substitute!(selector, @values.dup, true)
-
@selector = context.substitute!(selector, @values)
-
end
-
-
1
def extract_equality_tests
-
comparisons = {}
-
case comparator = @values.shift
-
when Hash
-
comparisons = comparator
-
when String, Regexp
-
comparisons[:text] = comparator
-
when Integer
-
comparisons[:count] = comparator
-
when Range
-
comparisons[:minimum] = comparator.begin
-
comparisons[:maximum] = comparator.end
-
when FalseClass
-
comparisons[:count] = 0
-
when NilClass, TrueClass
-
comparisons[:minimum] = 1
-
else raise ArgumentError, "I don't understand what you're trying to match"
-
end
-
-
# By default we're looking for at least one match.
-
if comparisons[:count]
-
comparisons[:minimum] = comparisons[:maximum] = comparisons[:count]
-
else
-
comparisons[:minimum] ||= 1
-
end
-
comparisons
-
end
-
end
-
1
class SubstitutionContext
-
1
def initialize
-
1
@substitute = '?'
-
end
-
-
1
def substitute!(selector, values, format_for_presentation = false)
-
selector = selector.dup
-
-
while !values.empty? && substitutable?(values.first) && selector.index(@substitute)
-
selector.sub! @substitute, matcher_for(values.shift, format_for_presentation)
-
end
-
-
selector
-
end
-
-
1
def match(matches, attribute, matcher)
-
matches.find_all { |node| node[attribute] =~ Regexp.new(matcher) }
-
end
-
-
1
private
-
1
def matcher_for(value, format_for_presentation)
-
# Nokogiri doesn't like arbitrary values without quotes, hence inspect.
-
if format_for_presentation
-
value.inspect # Avoid to_s so Regexps aren't put in quotes.
-
else
-
value.to_s.inspect
-
end
-
end
-
-
1
def substitutable?(value)
-
value.is_a?(String) || value.is_a?(Regexp)
-
end
-
end
-
1
RSpec::Support.require_rspec_core "formatters/helpers"
-
1
require 'stringio'
-
-
1
module RSpec
-
1
module Core
-
1
module Formatters
-
# RSpec's built-in formatters are all subclasses of
-
# RSpec::Core::Formatters::BaseFormatter.
-
#
-
# @see RSpec::Core::Formatters::BaseTextFormatter
-
# @see RSpec::Core::Reporter
-
# @see RSpec::Core::Formatters::Protocol
-
1
class BaseFormatter
-
# All formatters inheriting from this formatter will receive these
-
# notifications.
-
1
Formatters.register self, :start, :example_group_started, :close
-
1
attr_accessor :example_group
-
1
attr_reader :output
-
-
# @api public
-
# @param output [IO] the formatter output
-
# @see RSpec::Core::Formatters::Protocol#initialize
-
1
def initialize(output)
-
1
@output = output || StringIO.new
-
1
@example_group = nil
-
end
-
-
# @api public
-
#
-
# @param notification [StartNotification]
-
# @see RSpec::Core::Formatters::Protocol#start
-
1
def start(notification)
-
1
start_sync_output
-
1
@example_count = notification.count
-
end
-
-
# @api public
-
#
-
# @param notification [GroupNotification] containing example_group
-
# subclass of `RSpec::Core::ExampleGroup`
-
# @see RSpec::Core::Formatters::Protocol#example_group_started
-
1
def example_group_started(notification)
-
3
@example_group = notification.group
-
end
-
-
# @api public
-
#
-
# @param _notification [NullNotification] (Ignored)
-
# @see RSpec::Core::Formatters::Protocol#close
-
1
def close(_notification)
-
restore_sync_output
-
end
-
-
1
private
-
-
1
def start_sync_output
-
1
@old_sync, output.sync = output.sync, true if output_supports_sync
-
end
-
-
1
def restore_sync_output
-
output.sync = @old_sync if output_supports_sync && !output.closed?
-
end
-
-
1
def output_supports_sync
-
1
output.respond_to?(:sync=)
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_core "formatters/base_formatter"
-
-
1
module RSpec
-
1
module Core
-
1
module Formatters
-
# Base for all of RSpec's built-in formatters. See
-
# RSpec::Core::Formatters::BaseFormatter to learn more about all of the
-
# methods called by the reporter.
-
#
-
# @see RSpec::Core::Formatters::BaseFormatter
-
# @see RSpec::Core::Reporter
-
1
class BaseTextFormatter < BaseFormatter
-
1
Formatters.register self,
-
:message, :dump_summary, :dump_failures, :dump_pending, :seed
-
-
# @api public
-
#
-
# Used by the reporter to send messages to the output stream.
-
#
-
# @param notification [MessageNotification] containing message
-
1
def message(notification)
-
output.puts notification.message
-
end
-
-
# @api public
-
#
-
# Dumps detailed information about each example failure.
-
#
-
# @param notification [NullNotification]
-
1
def dump_failures(notification)
-
1
return if notification.failure_notifications.empty?
-
output.puts notification.fully_formatted_failed_examples
-
end
-
-
# @api public
-
#
-
# This method is invoked after the dumping of examples and failures.
-
# Each parameter is assigned to a corresponding attribute.
-
#
-
# @param summary [SummaryNotification] containing duration,
-
# example_count, failure_count and pending_count
-
1
def dump_summary(summary)
-
1
output.puts summary.fully_formatted
-
end
-
-
# @private
-
1
def dump_pending(notification)
-
1
return if notification.pending_examples.empty?
-
output.puts notification.fully_formatted_pending_examples
-
end
-
-
# @private
-
1
def seed(notification)
-
2
return unless notification.seed_used?
-
output.puts notification.fully_formatted
-
end
-
-
# @api public
-
#
-
# Invoked at the end of a suite run. Allows the formatter to do any
-
# tidying up, but be aware that formatter output streams may be used
-
# elsewhere so don't actually close them.
-
#
-
# @param _notification [NullNotification] (Ignored)
-
1
def close(_notification)
-
1
return if output.closed?
-
-
1
output.puts
-
-
1
output.flush
-
end
-
end
-
end
-
end
-
end
-
1
RSpec::Support.require_rspec_core "formatters/base_text_formatter"
-
1
RSpec::Support.require_rspec_core "formatters/console_codes"
-
-
1
module RSpec
-
1
module Core
-
1
module Formatters
-
# @private
-
1
class ProgressFormatter < BaseTextFormatter
-
1
Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump
-
-
1
def example_passed(_notification)
-
2
output.print ConsoleCodes.wrap('.', :success)
-
end
-
-
1
def example_pending(_notification)
-
output.print ConsoleCodes.wrap('*', :pending)
-
end
-
-
1
def example_failed(_notification)
-
output.print ConsoleCodes.wrap('F', :failure)
-
end
-
-
1
def start_dump(_notification)
-
1
output.puts
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Core
-
# @private
-
1
class Profiler
-
1
NOTIFICATIONS = [:example_group_started, :example_group_finished, :example_started]
-
-
1
def initialize
-
2
@example_groups = Hash.new { |h, k| h[k] = { :count => 0 } }
-
end
-
-
1
attr_reader :example_groups
-
-
1
def example_group_started(notification)
-
3
return unless notification.group.top_level?
-
-
1
@example_groups[notification.group][:start] = Time.now
-
1
@example_groups[notification.group][:description] = notification.group.top_level_description
-
end
-
-
1
def example_group_finished(notification)
-
3
return unless notification.group.top_level?
-
-
1
group = @example_groups[notification.group]
-
1
return unless group.key?(:start)
-
1
group[:total_time] = Time.now - group[:start]
-
end
-
-
1
def example_started(notification)
-
2
group = notification.example.example_group.parent_groups.last
-
2
@example_groups[group][:count] += 1
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
1
module BuiltIn
-
# @api private
-
# Provides the implementation for `be_truthy`.
-
# Not intended to be instantiated directly.
-
1
class BeTruthy < BaseMatcher
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"expected: truthy value\n got: #{actual_formatted}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected: falsey value\n got: #{actual_formatted}"
-
end
-
-
1
private
-
-
1
def match(_, actual)
-
!!actual
-
end
-
end
-
-
# @api private
-
# Provides the implementation for `be_falsey`.
-
# Not intended to be instantiated directly.
-
1
class BeFalsey < BaseMatcher
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"expected: falsey value\n got: #{actual_formatted}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected: truthy value\n got: #{actual_formatted}"
-
end
-
-
1
private
-
-
1
def match(_, actual)
-
!actual
-
end
-
end
-
-
# @api private
-
# Provides the implementation for `be_nil`.
-
# Not intended to be instantiated directly.
-
1
class BeNil < BaseMatcher
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"expected: nil\n got: #{actual_formatted}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected: not nil\n got: nil"
-
end
-
-
1
private
-
-
1
def match(_, actual)
-
actual.nil?
-
end
-
end
-
-
# @private
-
1
module BeHelpers
-
1
private
-
-
1
def args_to_s
-
@args.empty? ? "" : parenthesize(inspected_args.join(', '))
-
end
-
-
1
def parenthesize(string)
-
"(#{string})"
-
end
-
-
1
def inspected_args
-
@args.map { |a| RSpec::Support::ObjectFormatter.format(a) }
-
end
-
-
1
def expected_to_sentence
-
EnglishPhrasing.split_words(@expected)
-
end
-
-
1
def args_to_sentence
-
EnglishPhrasing.list(@args)
-
end
-
end
-
-
# @api private
-
# Provides the implementation for `be`.
-
# Not intended to be instantiated directly.
-
1
class Be < BaseMatcher
-
1
include BeHelpers
-
-
1
def initialize(*args)
-
@args = args
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"expected #{actual_formatted} to evaluate to true"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected #{actual_formatted} to evaluate to false"
-
end
-
-
1
[:==, :<, :<=, :>=, :>, :===, :=~].each do |operator|
-
7
define_method operator do |operand|
-
BeComparedTo.new(operand, operator)
-
end
-
end
-
-
1
private
-
-
1
def match(_, actual)
-
!!actual
-
end
-
end
-
-
# @api private
-
# Provides the implementation of `be <operator> value`.
-
# Not intended to be instantiated directly.
-
1
class BeComparedTo < BaseMatcher
-
1
include BeHelpers
-
-
1
def initialize(operand, operator)
-
@expected = operand
-
@operator = operator
-
@args = []
-
end
-
-
1
def matches?(actual)
-
perform_match(actual)
-
rescue ArgumentError, NoMethodError
-
false
-
end
-
-
1
def does_not_match?(actual)
-
!perform_match(actual)
-
rescue ArgumentError, NoMethodError
-
false
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"expected: #{@operator} #{expected_formatted}\n" \
-
" got: #{@operator.to_s.gsub(/./, ' ')} #{actual_formatted}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
message = "`expect(#{actual_formatted}).not_to " \
-
"be #{@operator} #{expected_formatted}`"
-
if [:<, :>, :<=, :>=].include?(@operator)
-
message + " not only FAILED, it is a bit confusing."
-
else
-
message
-
end
-
end
-
-
# @api private
-
# @return [String]
-
1
def description
-
"be #{@operator} #{expected_to_sentence}#{args_to_sentence}"
-
end
-
-
1
private
-
-
1
def perform_match(actual)
-
@actual = actual
-
@actual.__send__ @operator, @expected
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
1
module BuiltIn
-
# rubocop:disable ClassLength
-
# @api private
-
# Provides the implementation for `contain_exactly` and `match_array`.
-
# Not intended to be instantiated directly.
-
1
class ContainExactly < BaseMatcher
-
# @api private
-
# @return [String]
-
1
def failure_message
-
if Array === actual
-
generate_failure_message
-
else
-
"expected a collection that can be converted to an array with " \
-
"`#to_ary` or `#to_a`, but got #{actual_formatted}"
-
end
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
list = EnglishPhrasing.list(surface_descriptions_in(expected))
-
"expected #{actual_formatted} not to contain exactly#{list}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def description
-
list = EnglishPhrasing.list(surface_descriptions_in(expected))
-
"contain exactly#{list}"
-
end
-
-
1
private
-
-
1
def generate_failure_message
-
message = expected_collection_line
-
message += actual_collection_line
-
message += missing_elements_line unless missing_items.empty?
-
message += extra_elements_line unless extra_items.empty?
-
message
-
end
-
-
1
def expected_collection_line
-
message_line('expected collection contained', expected, true)
-
end
-
-
1
def actual_collection_line
-
message_line('actual collection contained', actual)
-
end
-
-
1
def missing_elements_line
-
message_line('the missing elements were', missing_items, true)
-
end
-
-
1
def extra_elements_line
-
message_line('the extra elements were', extra_items)
-
end
-
-
1
def describe_collection(collection, surface_descriptions=false)
-
if surface_descriptions
-
"#{description_of(safe_sort(surface_descriptions_in collection))}\n"
-
else
-
"#{description_of(safe_sort(collection))}\n"
-
end
-
end
-
-
1
def message_line(prefix, collection, surface_descriptions=false)
-
"%-32s%s" % [prefix + ':',
-
describe_collection(collection, surface_descriptions)]
-
end
-
-
1
def match(_expected, _actual)
-
return false unless convert_actual_to_an_array
-
match_when_sorted? || (extra_items.empty? && missing_items.empty?)
-
end
-
-
# This cannot always work (e.g. when dealing with unsortable items,
-
# or matchers as expected items), but it's practically free compared to
-
# the slowness of the full matching algorithm, and in common cases this
-
# works, so it's worth a try.
-
1
def match_when_sorted?
-
values_match?(safe_sort(expected), safe_sort(actual))
-
end
-
-
1
def convert_actual_to_an_array
-
if actual.respond_to?(:to_ary)
-
@actual = actual.to_ary
-
elsif actual.respond_to?(:to_a) && !to_a_disallowed?(actual)
-
@actual = actual.to_a
-
else
-
false
-
end
-
end
-
-
1
def safe_sort(array)
-
array.sort
-
rescue Support::AllExceptionsExceptOnesWeMustNotRescue
-
array
-
end
-
-
1
if RUBY_VERSION == "1.8.7"
-
def to_a_disallowed?(object)
-
case object
-
when NilClass, String then true
-
else Kernel == RSpec::Support.method_handle_for(object, :to_a).owner
-
end
-
end
-
else
-
1
def to_a_disallowed?(object)
-
NilClass === object
-
end
-
end
-
-
1
def missing_items
-
@missing_items ||= best_solution.unmatched_expected_indexes.map do |index|
-
expected[index]
-
end
-
end
-
-
1
def extra_items
-
@extra_items ||= best_solution.unmatched_actual_indexes.map do |index|
-
actual[index]
-
end
-
end
-
-
1
def best_solution
-
@best_solution ||= pairings_maximizer.find_best_solution
-
end
-
-
1
def pairings_maximizer
-
@pairings_maximizer ||= begin
-
expected_matches = Hash[Array.new(expected.size) { |i| [i, []] }]
-
actual_matches = Hash[Array.new(actual.size) { |i| [i, []] }]
-
-
expected.each_with_index do |e, ei|
-
actual.each_with_index do |a, ai|
-
next unless values_match?(e, a)
-
-
expected_matches[ei] << ai
-
actual_matches[ai] << ei
-
end
-
end
-
-
PairingsMaximizer.new(expected_matches, actual_matches)
-
end
-
end
-
-
# Once we started supporting composing matchers, the algorithm for this matcher got
-
# much more complicated. Consider this expression:
-
#
-
# expect(["fool", "food"]).to contain_exactly(/foo/, /fool/)
-
#
-
# This should pass (because we can pair /fool/ with "fool" and /foo/ with "food"), but
-
# the original algorithm used by this matcher would pair the first elements it could
-
# (/foo/ with "fool"), which would leave /fool/ and "food" unmatched. When we have
-
# an expected element which is a matcher that matches a superset of actual items
-
# compared to another expected element matcher, we need to consider every possible pairing.
-
#
-
# This class is designed to maximize the number of actual/expected pairings -- or,
-
# conversely, to minimize the number of unpaired items. It's essentially a brute
-
# force solution, but with a few heuristics applied to reduce the size of the
-
# problem space:
-
#
-
# * Any items which match none of the items in the other list are immediately
-
# placed into the `unmatched_expected_indexes` or `unmatched_actual_indexes` array.
-
# The extra items and missing items in the matcher failure message are derived
-
# from these arrays.
-
# * Any items which reciprocally match only each other are paired up and not
-
# considered further.
-
#
-
# What's left is only the items which match multiple items from the other list
-
# (or vice versa). From here, it performs a brute-force depth-first search,
-
# looking for a solution which pairs all elements in both lists, or, barring that,
-
# that produces the fewest unmatched items.
-
#
-
# @private
-
1
class PairingsMaximizer
-
# @private
-
1
Solution = Struct.new(:unmatched_expected_indexes, :unmatched_actual_indexes,
-
:indeterminate_expected_indexes, :indeterminate_actual_indexes) do
-
1
def worse_than?(other)
-
unmatched_item_count > other.unmatched_item_count
-
end
-
-
1
def candidate?
-
indeterminate_expected_indexes.empty? &&
-
indeterminate_actual_indexes.empty?
-
end
-
-
1
def ideal?
-
candidate? && (
-
unmatched_expected_indexes.empty? ||
-
unmatched_actual_indexes.empty?
-
)
-
end
-
-
1
def unmatched_item_count
-
unmatched_expected_indexes.count + unmatched_actual_indexes.count
-
end
-
-
1
def +(derived_candidate_solution)
-
self.class.new(
-
unmatched_expected_indexes + derived_candidate_solution.unmatched_expected_indexes,
-
unmatched_actual_indexes + derived_candidate_solution.unmatched_actual_indexes,
-
# Ignore the indeterminate indexes: by the time we get here,
-
# we've dealt with all indeterminates.
-
[], []
-
)
-
end
-
end
-
-
1
attr_reader :expected_to_actual_matched_indexes, :actual_to_expected_matched_indexes, :solution
-
-
1
def initialize(expected_to_actual_matched_indexes, actual_to_expected_matched_indexes)
-
@expected_to_actual_matched_indexes = expected_to_actual_matched_indexes
-
@actual_to_expected_matched_indexes = actual_to_expected_matched_indexes
-
-
unmatched_expected_indexes, indeterminate_expected_indexes =
-
categorize_indexes(expected_to_actual_matched_indexes, actual_to_expected_matched_indexes)
-
-
unmatched_actual_indexes, indeterminate_actual_indexes =
-
categorize_indexes(actual_to_expected_matched_indexes, expected_to_actual_matched_indexes)
-
-
@solution = Solution.new(unmatched_expected_indexes, unmatched_actual_indexes,
-
indeterminate_expected_indexes, indeterminate_actual_indexes)
-
end
-
-
1
def find_best_solution
-
return solution if solution.candidate?
-
best_solution_so_far = NullSolution
-
-
expected_index = solution.indeterminate_expected_indexes.first
-
actuals = expected_to_actual_matched_indexes[expected_index]
-
-
actuals.each do |actual_index|
-
solution = best_solution_for_pairing(expected_index, actual_index)
-
return solution if solution.ideal?
-
best_solution_so_far = solution if best_solution_so_far.worse_than?(solution)
-
end
-
-
best_solution_so_far
-
end
-
-
1
private
-
-
# @private
-
# Starting solution that is worse than any other real solution.
-
1
NullSolution = Class.new do
-
1
def self.worse_than?(_other)
-
true
-
end
-
end
-
-
1
def categorize_indexes(indexes_to_categorize, other_indexes)
-
unmatched = []
-
indeterminate = []
-
-
indexes_to_categorize.each_pair do |index, matches|
-
if matches.empty?
-
unmatched << index
-
elsif !reciprocal_single_match?(matches, index, other_indexes)
-
indeterminate << index
-
end
-
end
-
-
return unmatched, indeterminate
-
end
-
-
1
def reciprocal_single_match?(matches, index, other_list)
-
return false unless matches.one?
-
other_list[matches.first] == [index]
-
end
-
-
1
def best_solution_for_pairing(expected_index, actual_index)
-
modified_expecteds = apply_pairing_to(
-
solution.indeterminate_expected_indexes,
-
expected_to_actual_matched_indexes, actual_index)
-
-
modified_expecteds.delete(expected_index)
-
-
modified_actuals = apply_pairing_to(
-
solution.indeterminate_actual_indexes,
-
actual_to_expected_matched_indexes, expected_index)
-
-
modified_actuals.delete(actual_index)
-
-
solution + self.class.new(modified_expecteds, modified_actuals).find_best_solution
-
end
-
-
1
def apply_pairing_to(indeterminates, original_matches, other_list_index)
-
indeterminates.inject({}) do |accum, index|
-
accum[index] = original_matches[index] - [other_list_index]
-
accum
-
end
-
end
-
end
-
end
-
# rubocop:enable ClassLength
-
end
-
end
-
end
-
1
module RSpec
-
1
module Matchers
-
1
module BuiltIn
-
# @api private
-
# Provides the implementation for `eq`.
-
# Not intended to be instantiated directly.
-
1
class Eq < BaseMatcher
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"\nexpected: #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using ==)\n"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"\nexpected: value != #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using ==)\n"
-
end
-
-
# @api private
-
# @return [String]
-
1
def description
-
"eq #{expected_formatted}"
-
end
-
-
# @api private
-
# @return [Boolean]
-
1
def diffable?
-
true
-
end
-
-
1
private
-
-
1
def match(expected, actual)
-
1
actual == expected
-
end
-
end
-
end
-
end
-
end
-
1
require 'rspec/support'
-
-
1
module RSpec
-
1
module Matchers
-
1
module BuiltIn
-
# @api private
-
# Provides the implementation for operator matchers.
-
# Not intended to be instantiated directly.
-
# Only available for use with `should`.
-
1
class OperatorMatcher
-
1
class << self
-
# @private
-
1
def registry
-
4
@registry ||= {}
-
end
-
-
# @private
-
1
def register(klass, operator, matcher)
-
2
registry[klass] ||= {}
-
2
registry[klass][operator] = matcher
-
end
-
-
# @private
-
1
def unregister(klass, operator)
-
registry[klass] && registry[klass].delete(operator)
-
end
-
-
# @private
-
1
def get(klass, operator)
-
klass.ancestors.each do |ancestor|
-
matcher = registry[ancestor] && registry[ancestor][operator]
-
return matcher if matcher
-
end
-
-
nil
-
end
-
end
-
-
1
register Enumerable, '=~', BuiltIn::ContainExactly
-
-
1
def initialize(actual)
-
@actual = actual
-
end
-
-
# @private
-
1
def self.use_custom_matcher_or_delegate(operator)
-
7
define_method(operator) do |expected|
-
if !has_non_generic_implementation_of?(operator) && (matcher = OperatorMatcher.get(@actual.class, operator))
-
@actual.__send__(::RSpec::Matchers.last_expectation_handler.should_method, matcher.new(expected))
-
else
-
eval_match(@actual, operator, expected)
-
end
-
end
-
-
7
negative_operator = operator.sub(/^=/, '!')
-
7
if negative_operator != operator && respond_to?(negative_operator)
-
2
define_method(negative_operator) do |_expected|
-
opposite_should = ::RSpec::Matchers.last_expectation_handler.opposite_should_method
-
raise "RSpec does not support `#{::RSpec::Matchers.last_expectation_handler.should_method} #{negative_operator} expected`. " \
-
"Use `#{opposite_should} #{operator} expected` instead."
-
end
-
end
-
end
-
-
1
['==', '===', '=~', '>', '>=', '<', '<='].each do |operator|
-
7
use_custom_matcher_or_delegate operator
-
end
-
-
# @private
-
1
def fail_with_message(message)
-
RSpec::Expectations.fail_with(message, @expected, @actual)
-
end
-
-
# @api private
-
# @return [String]
-
1
def description
-
"#{@operator} #{RSpec::Support::ObjectFormatter.format(@expected)}"
-
end
-
-
1
private
-
-
1
def has_non_generic_implementation_of?(op)
-
Support.method_handle_for(@actual, op).owner != ::Kernel
-
rescue NameError
-
false
-
end
-
-
1
def eval_match(actual, operator, expected)
-
::RSpec::Matchers.last_matcher = self
-
@operator, @expected = operator, expected
-
__delegate_operator(actual, operator, expected)
-
end
-
end
-
-
# @private
-
# Handles operator matcher for `should`.
-
1
class PositiveOperatorMatcher < OperatorMatcher
-
1
def __delegate_operator(actual, operator, expected)
-
if actual.__send__(operator, expected)
-
true
-
else
-
expected_formatted = RSpec::Support::ObjectFormatter.format(expected)
-
actual_formatted = RSpec::Support::ObjectFormatter.format(actual)
-
-
if ['==', '===', '=~'].include?(operator)
-
fail_with_message("expected: #{expected_formatted}\n got: #{actual_formatted} (using #{operator})")
-
else
-
fail_with_message("expected: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}")
-
end
-
end
-
end
-
end
-
-
# @private
-
# Handles operator matcher for `should_not`.
-
1
class NegativeOperatorMatcher < OperatorMatcher
-
1
def __delegate_operator(actual, operator, expected)
-
return false unless actual.__send__(operator, expected)
-
-
expected_formatted = RSpec::Support::ObjectFormatter.format(expected)
-
actual_formatted = RSpec::Support::ObjectFormatter.format(actual)
-
-
fail_with_message("expected not: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}")
-
end
-
end
-
end
-
end
-
end
-
1
require 'rspec/core'
-
1
require 'rails/version'
-
-
# Load any of our adapters and extensions early in the process
-
1
require 'rspec/rails/adapters'
-
1
require 'rspec/rails/extensions'
-
-
# Load the rspec-rails parts
-
1
require 'rspec/rails/view_rendering'
-
1
require 'rspec/rails/matchers'
-
1
require 'rspec/rails/fixture_support'
-
1
require 'rspec/rails/file_fixture_support'
-
1
require 'rspec/rails/fixture_file_upload_support'
-
1
require 'rspec/rails/example'
-
1
require 'rspec/rails/vendor/capybara'
-
1
require 'rspec/rails/configuration'
-
1
require 'rspec/rails/active_record'
-
1
require 'rspec/rails/feature_check'
-
1
module RSpec
-
1
module Rails
-
# Fake class to document RSpec ActiveRecord configuration options. In practice,
-
# these are dynamically added to the normal RSpec configuration object.
-
1
class ActiveRecordConfiguration
-
# @private
-
1
def self.initialize_activerecord_configuration(config)
-
1
config.before :suite do
-
# This allows dynamic columns etc to be used on ActiveRecord models when creating instance_doubles
-
1
if defined?(ActiveRecord) && defined?(ActiveRecord::Base) && defined?(::RSpec::Mocks) && (::RSpec::Mocks.respond_to?(:configuration))
-
1
::RSpec::Mocks.configuration.when_declaring_verifying_double do |possible_model|
-
target = possible_model.target
-
-
if Class === target && ActiveRecord::Base > target && !target.abstract_class?
-
target.define_attribute_methods
-
end
-
end
-
end
-
end
-
end
-
-
1
initialize_activerecord_configuration RSpec.configuration
-
end
-
end
-
end
-
1
require 'delegate'
-
1
require 'active_support'
-
1
require 'active_support/concern'
-
1
require 'active_support/core_ext/string'
-
-
1
module RSpec
-
1
module Rails
-
# @private
-
1
def self.disable_testunit_autorun
-
# `Test::Unit::AutoRunner.need_auto_run=` was introduced to the test-unit
-
# gem in version 2.4.9. Previous to this version `Test::Unit.run=` was
-
# used. The implementation of test-unit included with Ruby has neither
-
# method.
-
if defined?(Test::Unit::AutoRunner.need_auto_run = ())
-
Test::Unit::AutoRunner.need_auto_run = false
-
elsif defined?(Test::Unit.run = ())
-
Test::Unit.run = false
-
end
-
end
-
1
private_class_method :disable_testunit_autorun
-
-
1
if defined?(Kernel.gem)
-
1
gem 'minitest'
-
else
-
require 'minitest'
-
end
-
1
require 'minitest/assertions'
-
# Constant aliased to either Minitest or TestUnit, depending on what is
-
# loaded.
-
1
Assertions = Minitest::Assertions
-
-
# @private
-
1
class AssertionDelegator < Module
-
1
def initialize(*assertion_modules)
-
2
assertion_class = Class.new(SimpleDelegator) do
-
2
include ::RSpec::Rails::Assertions
-
2
include ::RSpec::Rails::MinitestCounters
-
4
assertion_modules.each { |mod| include mod }
-
end
-
-
2
super() do
-
2
define_method :build_assertion_instance do
-
assertion_class.new(self)
-
end
-
-
2
def assertion_instance
-
@assertion_instance ||= build_assertion_instance
-
end
-
-
2
assertion_modules.each do |mod|
-
2
mod.public_instance_methods.each do |method|
-
12
next if method == :method_missing || method == "method_missing"
-
-
10
define_method(method.to_sym) do |*args, &block|
-
assertion_instance.send(method.to_sym, *args, &block)
-
end
-
end
-
end
-
end
-
end
-
end
-
-
# Adapts example groups for `Minitest::Test::LifecycleHooks`
-
#
-
# @private
-
1
module MinitestLifecycleAdapter
-
1
extend ActiveSupport::Concern
-
-
1
included do |group|
-
3
group.before { after_setup }
-
3
group.after { before_teardown }
-
-
1
group.around do |example|
-
2
before_setup
-
2
example.run
-
2
after_teardown
-
end
-
end
-
-
1
def before_setup
-
end
-
-
1
def after_setup
-
end
-
-
1
def before_teardown
-
end
-
-
1
def after_teardown
-
end
-
end
-
-
# @private
-
1
module MinitestCounters
-
1
attr_writer :assertions
-
1
def assertions
-
@assertions ||= 0
-
end
-
end
-
-
# @private
-
1
module SetupAndTeardownAdapter
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
# Wraps `setup` calls from within Rails' testing framework in `before`
-
# hooks.
-
1
def setup(*methods, &block)
-
methods.each do |method|
-
if method.to_s =~ /^setup_(with_controller|fixtures|controller_request_and_response)$/
-
prepend_before { __send__ method }
-
else
-
before { __send__ method }
-
end
-
end
-
before(&block) if block
-
end
-
-
# @api private
-
#
-
# Wraps `teardown` calls from within Rails' testing framework in
-
# `after` hooks.
-
1
def teardown(*methods, &block)
-
methods.each { |method| after { __send__ method } }
-
after(&block) if block
-
end
-
end
-
-
1
def initialize(*args)
-
8
super
-
8
@example = nil
-
end
-
-
1
def method_name
-
@example
-
end
-
end
-
-
# @private
-
1
module MinitestAssertionAdapter
-
1
extend ActiveSupport::Concern
-
-
# @private
-
1
module ClassMethods
-
# Returns the names of assertion methods that we want to expose to
-
# examples without exposing non-assertion methods in Test::Unit or
-
# Minitest.
-
1
def assertion_method_names
-
1
::RSpec::Rails::Assertions
-
.public_instance_methods
-
.select do |m|
-
50
m.to_s =~ /^(assert|flunk|refute)/
-
end
-
end
-
-
1
def define_assertion_delegators
-
1
assertion_method_names.each do |m|
-
36
define_method(m.to_sym) do |*args, &block|
-
assertion_delegator.send(m.to_sym, *args, &block)
-
end
-
end
-
end
-
end
-
-
1
class AssertionDelegator
-
1
include ::RSpec::Rails::Assertions
-
1
include ::RSpec::Rails::MinitestCounters
-
end
-
-
1
def assertion_delegator
-
@assertion_delegator ||= AssertionDelegator.new
-
end
-
-
1
included do
-
1
define_assertion_delegators
-
end
-
end
-
-
# Backwards compatibility. It's unlikely that anyone is using this
-
# constant, but we had forgotten to mark it as `@private` earlier
-
#
-
# @private
-
1
TestUnitAssertionAdapter = MinitestAssertionAdapter
-
end
-
end
-
# rubocop: disable Metrics/ModuleLength
-
1
module RSpec
-
1
module Rails
-
# Fake class to document RSpec Rails configuration options. In practice,
-
# these are dynamically added to the normal RSpec configuration object.
-
1
class Configuration
-
# @!method infer_spec_type_from_file_location!
-
# Automatically tag specs in conventional directories with matching `type`
-
# metadata so that they have relevant helpers available to them. See
-
# `RSpec::Rails::DIRECTORY_MAPPINGS` for details on which metadata is
-
# applied to each directory.
-
-
# @!method render_views=(val)
-
#
-
# When set to `true`, controller specs will render the relevant view as
-
# well. Defaults to `false`.
-
-
# @!method render_views(val)
-
# Enables view rendering for controllers specs.
-
-
# @!method render_views?
-
# Reader for currently value of `render_views` setting.
-
end
-
-
# Mappings used by `infer_spec_type_from_file_location!`.
-
#
-
# @api private
-
DIRECTORY_MAPPINGS = {
-
1
channel: %w[spec channels],
-
controller: %w[spec controllers],
-
helper: %w[spec helpers],
-
job: %w[spec jobs],
-
mailer: %w[spec mailers],
-
model: %w[spec models],
-
request: %w[spec (requests|integration|api)],
-
routing: %w[spec routing],
-
view: %w[spec views],
-
feature: %w[spec features],
-
system: %w[spec system],
-
mailbox: %w[spec mailboxes]
-
}
-
-
# Sets up the different example group modules for the different spec types
-
#
-
# @api private
-
1
def self.add_test_type_configurations(config)
-
1
config.include RSpec::Rails::ControllerExampleGroup, type: :controller
-
1
config.include RSpec::Rails::HelperExampleGroup, type: :helper
-
1
config.include RSpec::Rails::ModelExampleGroup, type: :model
-
1
config.include RSpec::Rails::RequestExampleGroup, type: :request
-
1
config.include RSpec::Rails::RoutingExampleGroup, type: :routing
-
1
config.include RSpec::Rails::ViewExampleGroup, type: :view
-
1
config.include RSpec::Rails::FeatureExampleGroup, type: :feature
-
1
config.include RSpec::Rails::Matchers
-
1
config.include RSpec::Rails::SystemExampleGroup, type: :system
-
end
-
-
# @private
-
1
def self.initialize_configuration(config) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity
-
1
config.backtrace_exclusion_patterns << /vendor\//
-
1
config.backtrace_exclusion_patterns << %r{lib/rspec/rails}
-
-
# controller settings
-
1
config.add_setting :infer_base_class_for_anonymous_controllers, default: true
-
-
# fixture support
-
1
config.add_setting :use_active_record, default: true
-
1
config.add_setting :use_transactional_fixtures, alias_with: :use_transactional_examples
-
1
config.add_setting :use_instantiated_fixtures
-
1
config.add_setting :global_fixtures
-
1
config.add_setting :fixture_path
-
1
config.include RSpec::Rails::FixtureSupport, :use_fixtures
-
-
# We'll need to create a deprecated module in order to properly report to
-
# gems / projects which are relying on this being loaded globally.
-
#
-
# See rspec/rspec-rails#1355 for history
-
#
-
# @deprecated Include `RSpec::Rails::RailsExampleGroup` or
-
# `RSpec::Rails::FixtureSupport` directly instead
-
1
config.include RSpec::Rails::FixtureSupport
-
-
1
if ::Rails::VERSION::STRING > '5'
-
1
config.add_setting :file_fixture_path, default: 'spec/fixtures/files'
-
1
config.include RSpec::Rails::FileFixtureSupport
-
end
-
-
# Add support for fixture_path on fixture_file_upload
-
1
config.include RSpec::Rails::FixtureFileUploadSupport
-
-
# This allows us to expose `render_views` as a config option even though it
-
# breaks the convention of other options by using `render_views` as a
-
# command (i.e. `render_views = true`), where it would normally be used
-
# as a getter. This makes it easier for rspec-rails users because we use
-
# `render_views` directly in example groups, so this aligns the two APIs,
-
# but requires this workaround:
-
1
config.add_setting :rendering_views, default: false
-
-
1
config.instance_exec do
-
1
def render_views=(val)
-
self.rendering_views = val
-
end
-
-
1
def render_views
-
self.rendering_views = true
-
end
-
-
1
def render_views?
-
rendering_views?
-
end
-
-
1
undef :rendering_views? if respond_to?(:rendering_views?)
-
1
def rendering_views?
-
!!rendering_views
-
end
-
-
# Define boolean predicates rather than relying on rspec-core due
-
# to the bug fix in rspec/rspec-core#2736, note some of these
-
# predicates are a bit nonsensical, but they exist for backwards
-
# compatibility, we can tidy these up in `rspec-rails` 5.
-
1
undef :fixture_path? if respond_to?(:fixture_path?)
-
1
def fixture_path?
-
!!fixture_path
-
end
-
-
1
undef :global_fixtures? if respond_to?(:global_fixtures?)
-
1
def global_fixtures?
-
!!global_fixtures
-
end
-
-
1
undef :infer_base_class_for_anonymous_controllers? if respond_to?(:infer_base_class_for_anonymous_controllers?)
-
1
def infer_base_class_for_anonymous_controllers?
-
!!infer_base_class_for_anonymous_controllers
-
end
-
-
1
undef :use_instantiated_fixtures? if respond_to?(:use_instantiated_fixtures?)
-
1
def use_instantiated_fixtures?
-
!!use_instantiated_fixtures
-
end
-
-
1
undef :use_transactional_fixtures? if respond_to?(:use_transactional_fixtures?)
-
1
def use_transactional_fixtures?
-
!!use_transactional_fixtures
-
end
-
-
1
def infer_spec_type_from_file_location!
-
1
DIRECTORY_MAPPINGS.each do |type, dir_parts|
-
12
escaped_path = Regexp.compile(dir_parts.join('[\\\/]') + '[\\\/]')
-
12
define_derived_metadata(file_path: escaped_path) do |metadata|
-
6
metadata[:type] ||= type
-
end
-
end
-
end
-
-
# Adds exclusion filters for gems included with Rails
-
1
def filter_rails_from_backtrace!
-
1
filter_gems_from_backtrace "actionmailer", "actionpack", "actionview"
-
1
filter_gems_from_backtrace "activemodel", "activerecord",
-
"activesupport", "activejob"
-
end
-
end
-
-
1
add_test_type_configurations(config)
-
-
1
if defined?(::Rails::Controller::Testing)
-
[:controller, :view, :request].each do |type|
-
config.include ::Rails::Controller::Testing::TestProcess, type: type
-
config.include ::Rails::Controller::Testing::TemplateAssertions, type: type
-
config.include ::Rails::Controller::Testing::Integration, type: type
-
end
-
end
-
-
1
if RSpec::Rails::FeatureCheck.has_action_mailer?
-
1
config.include RSpec::Rails::MailerExampleGroup, type: :mailer
-
3
config.after { ActionMailer::Base.deliveries.clear }
-
end
-
-
1
if RSpec::Rails::FeatureCheck.has_active_job?
-
1
config.include RSpec::Rails::JobExampleGroup, type: :job
-
end
-
-
1
if RSpec::Rails::FeatureCheck.has_action_cable_testing?
-
1
config.include RSpec::Rails::ChannelExampleGroup, type: :channel
-
end
-
-
1
if RSpec::Rails::FeatureCheck.has_action_mailbox?
-
1
config.include RSpec::Rails::MailboxExampleGroup, type: :mailbox
-
end
-
end
-
-
1
initialize_configuration RSpec.configuration
-
end
-
end
-
# rubocop: enable Metrics/ModuleLength
-
1
require 'rspec/rails/example/rails_example_group'
-
1
require 'rspec/rails/example/controller_example_group'
-
1
require 'rspec/rails/example/request_example_group'
-
1
require 'rspec/rails/example/helper_example_group'
-
1
require 'rspec/rails/example/view_example_group'
-
1
require 'rspec/rails/example/mailer_example_group'
-
1
require 'rspec/rails/example/routing_example_group'
-
1
require 'rspec/rails/example/model_example_group'
-
1
require 'rspec/rails/example/job_example_group'
-
1
require 'rspec/rails/example/feature_example_group'
-
1
require 'rspec/rails/example/system_example_group'
-
1
require 'rspec/rails/example/channel_example_group'
-
1
require 'rspec/rails/example/mailbox_example_group'
-
1
require "rspec/rails/matchers/action_cable/have_streams"
-
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for channel spec functionality. It is only available if
-
# ActionCable has been loaded before it.
-
1
module ChannelExampleGroup
-
# @private
-
1
module ClassMethods
-
# These blank modules are only necessary for YARD processing. It doesn't
-
# handle the conditional check below very well and reports undocumented objects.
-
end
-
end
-
end
-
end
-
-
1
if RSpec::Rails::FeatureCheck.has_action_cable_testing?
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for channel spec functionality.
-
1
module ChannelExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include ActionCable::Connection::TestCase::Behavior
-
1
include ActionCable::Channel::TestCase::Behavior
-
-
# Class-level DSL for channel specs.
-
1
module ClassMethods
-
# @private
-
1
def channel_class
-
(_channel_class || described_class).tap do |klass|
-
next if klass <= ::ActionCable::Channel::Base
-
-
raise "Described class is not a channel class.\n" \
-
"Specify the channel class in the `describe` statement " \
-
"or set it manually using `tests MyChannelClass`"
-
end
-
end
-
-
# @private
-
1
def connection_class
-
(_connection_class || described_class).tap do |klass|
-
next if klass <= ::ActionCable::Connection::Base
-
-
raise "Described class is not a connection class.\n" \
-
"Specify the connection class in the `describe` statement " \
-
"or set it manually using `tests MyConnectionClass`"
-
end
-
end
-
end
-
-
# Checks that the connection attempt has been rejected.
-
#
-
# @example
-
# expect { connect }.to have_rejected_connection
-
1
def have_rejected_connection
-
raise_error(::ActionCable::Connection::Authorization::UnauthorizedError)
-
end
-
-
# Checks that the subscription is subscribed to at least one stream.
-
#
-
# @example
-
# expect(subscription).to have_streams
-
1
def have_streams
-
check_subscribed!
-
-
RSpec::Rails::Matchers::ActionCable::HaveStream.new
-
end
-
-
# Checks that the channel has been subscribed to the given stream
-
#
-
# @example
-
# expect(subscription).to have_stream_from("chat_1")
-
1
def have_stream_from(stream)
-
check_subscribed!
-
-
RSpec::Rails::Matchers::ActionCable::HaveStream.new(stream)
-
end
-
-
# Checks that the channel has been subscribed to a stream for the given model
-
#
-
# @example
-
# expect(subscription).to have_stream_for(user)
-
1
def have_stream_for(object)
-
check_subscribed!
-
RSpec::Rails::Matchers::ActionCable::HaveStream.new(broadcasting_for(object))
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @private
-
1
ControllerAssertionDelegator = RSpec::Rails::AssertionDelegator.new(
-
ActionDispatch::Assertions::RoutingAssertions
-
)
-
-
# @api public
-
# Container module for controller spec functionality.
-
1
module ControllerExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include ActionController::TestCase::Behavior
-
1
include RSpec::Rails::ViewRendering
-
1
include RSpec::Rails::Matchers::RedirectTo
-
1
include RSpec::Rails::Matchers::RenderTemplate
-
1
include RSpec::Rails::Matchers::RoutingMatchers
-
1
include ControllerAssertionDelegator
-
-
# Class-level DSL for controller specs.
-
1
module ClassMethods
-
# @private
-
1
def controller_class
-
described_class
-
end
-
-
# Supports a simple DSL for specifying behavior of ApplicationController.
-
# Creates an anonymous subclass of ApplicationController and evals the
-
# `body` in that context. Also sets up implicit routes for this
-
# controller, that are separate from those defined in "config/routes.rb".
-
#
-
# @note Due to Ruby 1.8 scoping rules in anonymous subclasses, constants
-
# defined in `ApplicationController` must be fully qualified (e.g.
-
# `ApplicationController::AccessDenied`) in the block passed to the
-
# `controller` method. Any instance methods, filters, etc, that are
-
# defined in `ApplicationController`, however, are accessible from
-
# within the block.
-
#
-
# @example
-
# describe ApplicationController do
-
# controller do
-
# def index
-
# raise ApplicationController::AccessDenied
-
# end
-
# end
-
#
-
# describe "handling AccessDenied exceptions" do
-
# it "redirects to the /401.html page" do
-
# get :index
-
# response.should redirect_to("/401.html")
-
# end
-
# end
-
# end
-
#
-
# If you would like to spec a subclass of ApplicationController, call
-
# controller like so:
-
#
-
# controller(ApplicationControllerSubclass) do
-
# # ....
-
# end
-
1
def controller(base_class = nil, &body)
-
if RSpec.configuration.infer_base_class_for_anonymous_controllers?
-
base_class ||= controller_class
-
end
-
base_class ||= defined?(ApplicationController) ? ApplicationController : ActionController::Base
-
-
new_controller_class = Class.new(base_class) do
-
def self.name
-
root_controller = defined?(ApplicationController) ? ApplicationController : ActionController::Base
-
if superclass == root_controller || superclass.abstract?
-
"AnonymousController"
-
else
-
superclass.name
-
end
-
end
-
end
-
new_controller_class.class_exec(&body)
-
(class << self; self; end).__send__(:define_method, :controller_class) { new_controller_class }
-
-
before do
-
@orig_routes = routes
-
resource_name = if @controller.respond_to?(:controller_name)
-
@controller.controller_name.to_sym
-
else
-
:anonymous
-
end
-
resource_path = if @controller.respond_to?(:controller_path)
-
@controller.controller_path
-
else
-
resource_name.to_s
-
end
-
resource_module = resource_path.rpartition('/').first.presence
-
resource_as = 'anonymous_' + resource_path.tr('/', '_')
-
self.routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
-
r.draw do
-
resources resource_name,
-
as: resource_as,
-
module: resource_module,
-
path: resource_path
-
end
-
end
-
end
-
-
after do
-
self.routes = @orig_routes
-
@orig_routes = nil
-
end
-
end
-
-
# Specifies the routeset that will be used for the example group. This
-
# is most useful when testing Rails engines.
-
#
-
# @example
-
# describe MyEngine::PostsController do
-
# routes { MyEngine::Engine.routes }
-
#
-
# # ...
-
# end
-
1
def routes
-
before do
-
self.routes = yield
-
end
-
end
-
end
-
-
# @!attribute [r]
-
# Returns the controller object instance under test.
-
1
attr_reader :controller
-
-
# @!attribute [r]
-
# Returns the Rails routes used for the spec.
-
1
attr_reader :routes
-
-
# @private
-
#
-
# RSpec Rails uses this to make Rails routes easily available to specs.
-
1
def routes=(routes)
-
@routes = routes
-
assertion_instance.instance_variable_set(:@routes, routes)
-
end
-
-
# @private
-
1
module BypassRescue
-
1
def rescue_with_handler(exception)
-
raise exception
-
end
-
end
-
-
# Extends the controller with a module that overrides
-
# `rescue_with_handler` to raise the exception passed to it. Use this to
-
# specify that an action _should_ raise an exception given appropriate
-
# conditions.
-
#
-
# @example
-
# describe ProfilesController do
-
# it "raises a 403 when a non-admin user tries to view another user's profile" do
-
# profile = create_profile
-
# login_as profile.user
-
#
-
# expect do
-
# bypass_rescue
-
# get :show, id: profile.id + 1
-
# end.to raise_error(/403 Forbidden/)
-
# end
-
# end
-
1
def bypass_rescue
-
controller.extend(BypassRescue)
-
end
-
-
# If method is a named_route, delegates to the RouteSet associated with
-
# this controller.
-
1
def method_missing(method, *args, &block)
-
if route_available?(method)
-
controller.send(method, *args, &block)
-
else
-
super
-
end
-
end
-
-
1
included do
-
subject { controller }
-
-
before do
-
self.routes = ::Rails.application.routes
-
end
-
-
around do |ex|
-
previous_allow_forgery_protection_value = ActionController::Base.allow_forgery_protection
-
begin
-
ActionController::Base.allow_forgery_protection = false
-
ex.call
-
ensure
-
ActionController::Base.allow_forgery_protection = previous_allow_forgery_protection_value
-
end
-
end
-
end
-
-
1
private
-
-
1
def route_available?(method)
-
(defined?(@routes) && route_defined?(routes, method)) ||
-
(defined?(@orig_routes) && route_defined?(@orig_routes, method))
-
end
-
-
1
def route_defined?(routes, method)
-
return false if routes.nil?
-
-
if routes.named_routes.respond_to?(:route_defined?)
-
routes.named_routes.route_defined?(method)
-
else
-
routes.named_routes.helpers.include?(method)
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for routing spec functionality.
-
1
module FeatureExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
-
# Default host to be used in Rails route helpers if none is specified.
-
1
DEFAULT_HOST = "www.example.com"
-
-
1
included do
-
app = ::Rails.application
-
if app.respond_to?(:routes)
-
include app.routes.url_helpers if app.routes.respond_to?(:url_helpers)
-
include app.routes.mounted_helpers if app.routes.respond_to?(:mounted_helpers)
-
-
if respond_to?(:default_url_options)
-
default_url_options[:host] ||= ::RSpec::Rails::FeatureExampleGroup::DEFAULT_HOST
-
end
-
end
-
end
-
-
# Shim to check for presence of Capybara. Will delegate if present, raise
-
# if not. We assume here that in most cases `visit` will be the first
-
# Capybara method called in a spec.
-
1
def visit(*)
-
if defined?(super)
-
super
-
else
-
raise "Capybara not loaded, please add it to your Gemfile:\n\ngem \"capybara\""
-
end
-
end
-
end
-
end
-
end
-
-
1
unless RSpec.respond_to?(:feature)
-
opts = {
-
1
capybara_feature: true,
-
type: :feature,
-
1
skip: <<-EOT.squish
-
Feature specs require the Capybara (https://github.com/teamcapybara/capybara)
-
gem, version 2.13.0 or later.
-
EOT
-
}
-
-
1
RSpec.configure do |c|
-
1
c.alias_example_group_to :feature, opts
-
1
c.alias_example_to :scenario
-
1
c.alias_example_to :xscenario, skip: 'Temporarily skipped with xscenario'
-
end
-
end
-
1
require 'rspec/rails/view_assigns'
-
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for helper specs.
-
1
module HelperExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include ActionView::TestCase::Behavior
-
1
include RSpec::Rails::ViewAssigns
-
-
# @private
-
1
module ClassMethods
-
1
def determine_constant_from_test_name(_ignore)
-
described_class if yield(described_class)
-
end
-
end
-
-
# Returns an instance of ActionView::Base with the helper being specified
-
# mixed in, along with any of the built-in rails helpers.
-
1
def helper
-
_view.tap do |v|
-
v.extend(ApplicationHelper) if defined?(ApplicationHelper)
-
v.assign(view_assigns)
-
end
-
end
-
-
1
private
-
-
1
def _controller_path(example)
-
example.example_group.described_class.to_s.sub(/Helper/, '').underscore
-
end
-
-
1
included do
-
before do |example|
-
controller.controller_path = _controller_path(example)
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for job spec functionality. It is only available if
-
# ActiveJob has been loaded before it.
-
1
module JobExampleGroup
-
# This blank module is only necessary for YARD processing. It doesn't
-
# handle the conditional `defined?` check below very well.
-
end
-
end
-
end
-
-
1
if defined?(ActiveJob)
-
1
module RSpec
-
1
module Rails
-
# Container module for job spec functionality.
-
1
module JobExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for mailbox spec functionality.
-
1
module MailboxExampleGroup
-
1
extend ActiveSupport::Concern
-
-
1
if RSpec::Rails::FeatureCheck.has_action_mailbox?
-
1
require 'action_mailbox/test_helper'
-
1
extend ::ActionMailbox::TestHelper
-
-
# @private
-
1
def self.create_inbound_email(arg)
-
case arg
-
when Hash
-
create_inbound_email_from_mail(**arg)
-
else
-
create_inbound_email_from_source(arg.to_s)
-
end
-
end
-
else
-
def self.create_inbound_email(_arg)
-
raise "Could not load ActionMailer::TestHelper"
-
end
-
end
-
-
1
class_methods do
-
# @private
-
1
def mailbox_class
-
described_class
-
end
-
end
-
-
1
included do
-
subject { described_class }
-
end
-
-
# @api public
-
# Passes if the inbound email was delivered
-
#
-
# @example
-
# inbound_email = process(args)
-
# expect(inbound_email).to have_been_delivered
-
1
def have_been_delivered
-
satisfy('have been delivered', &:delivered?)
-
end
-
-
# @api public
-
# Passes if the inbound email bounced during processing
-
#
-
# @example
-
# inbound_email = process(args)
-
# expect(inbound_email).to have_bounced
-
1
def have_bounced
-
satisfy('have bounced', &:bounced?)
-
end
-
-
# @api public
-
# Passes if the inbound email failed to process
-
#
-
# @example
-
# inbound_email = process(args)
-
# expect(inbound_email).to have_failed
-
1
def have_failed
-
satisfy('have failed', &:failed?)
-
end
-
-
# Process an inbound email message directly, bypassing routing.
-
#
-
# @param message [Hash, Mail::Message] a mail message or hash of
-
# attributes used to build one
-
# @return [ActionMaibox::InboundMessage]
-
1
def process(message)
-
MailboxExampleGroup.create_inbound_email(message).tap do |mail|
-
self.class.mailbox_class.receive(mail)
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for mailer spec functionality. It is only available if
-
# ActionMailer has been loaded before it.
-
1
module MailerExampleGroup
-
# This blank module is only necessary for YARD processing. It doesn't
-
# handle the conditional `defined?` check below very well.
-
end
-
end
-
end
-
-
1
if defined?(ActionMailer)
-
1
module RSpec
-
1
module Rails
-
# Container module for mailer spec functionality.
-
1
module MailerExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include ActionMailer::TestCase::Behavior
-
-
1
included do
-
include ::Rails.application.routes.url_helpers
-
options = ::Rails.configuration.action_mailer.default_url_options || {}
-
options.each { |key, value| default_url_options[key] = value }
-
end
-
-
# Class-level DSL for mailer specs.
-
1
module ClassMethods
-
# Alias for `described_class`.
-
1
def mailer_class
-
described_class
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container class for model spec functionality. Does not provide anything
-
# special over the common RailsExampleGroup currently.
-
1
module ModelExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
end
-
end
-
end
-
# Temporary workaround to resolve circular dependency between rspec-rails' spec
-
# suite and ammeter.
-
1
require 'rspec/rails/matchers'
-
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Common rails example functionality.
-
1
module RailsExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::SetupAndTeardownAdapter
-
1
include RSpec::Rails::MinitestLifecycleAdapter
-
1
include RSpec::Rails::MinitestAssertionAdapter
-
1
include RSpec::Rails::FixtureSupport
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container class for request spec functionality.
-
1
module RequestExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include ActionDispatch::Integration::Runner
-
1
include ActionDispatch::Assertions
-
1
include RSpec::Rails::Matchers::RedirectTo
-
1
include RSpec::Rails::Matchers::RenderTemplate
-
1
include ActionController::TemplateAssertions
-
-
1
if ActionPack::VERSION::MAJOR >= 5
-
1
include ActionDispatch::IntegrationTest::Behavior
-
end
-
-
# Delegates to `Rails.application`.
-
1
def app
-
::Rails.application
-
end
-
-
1
included do
-
before do
-
@routes = ::Rails.application.routes
-
end
-
end
-
end
-
end
-
end
-
1
require "action_dispatch/testing/assertions/routing"
-
-
1
module RSpec
-
1
module Rails
-
# @private
-
1
RoutingAssertionDelegator = RSpec::Rails::AssertionDelegator.new(
-
ActionDispatch::Assertions::RoutingAssertions
-
)
-
-
# @api public
-
# Container module for routing spec functionality.
-
1
module RoutingExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include RSpec::Rails::Matchers::RoutingMatchers
-
1
include RSpec::Rails::Matchers::RoutingMatchers::RouteHelpers
-
1
include RSpec::Rails::RoutingAssertionDelegator
-
-
# Class-level DSL for route specs.
-
1
module ClassMethods
-
# Specifies the routeset that will be used for the example group. This
-
# is most useful when testing Rails engines.
-
#
-
# @example
-
# describe MyEngine::PostsController do
-
# routes { MyEngine::Engine.routes }
-
#
-
# it "routes posts#index" do
-
# expect(:get => "/posts").to
-
# route_to(:controller => "my_engine/posts", :action => "index")
-
# end
-
# end
-
1
def routes
-
before do
-
self.routes = yield
-
end
-
end
-
end
-
-
1
included do
-
before do
-
self.routes = ::Rails.application.routes
-
end
-
end
-
-
# @!attribute [r]
-
# @private
-
1
attr_reader :routes
-
-
# @private
-
1
def routes=(routes)
-
@routes = routes
-
assertion_instance.instance_variable_set(:@routes, routes)
-
end
-
-
1
private
-
-
1
def method_missing(m, *args, &block)
-
routes.url_helpers.respond_to?(m) ? routes.url_helpers.send(m, *args) : super
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container class for system tests
-
1
module SystemExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include RSpec::Rails::Matchers::RedirectTo
-
1
include RSpec::Rails::Matchers::RenderTemplate
-
1
include ActionDispatch::Integration::Runner
-
1
include ActionDispatch::Assertions
-
1
include ActionController::TemplateAssertions
-
-
# Special characters to translate into underscores for #method_name
-
1
CHARS_TO_TRANSLATE = ['/', '.', ':', ',', "'", '"', " "].freeze
-
-
# @private
-
1
module BlowAwayTeardownHooks
-
# @private
-
1
def before_teardown
-
end
-
-
# @private
-
1
def after_teardown
-
end
-
end
-
-
# for the SystemTesting Screenshot situation
-
1
def passed?
-
return false if RSpec.current_example.exception
-
return true unless defined?(::RSpec::Expectations::FailureAggregator)
-
-
failure_notifier = ::RSpec::Support.failure_notifier
-
return true unless failure_notifier.is_a?(::RSpec::Expectations::FailureAggregator)
-
-
failure_notifier.failures.empty? && failure_notifier.other_errors.empty?
-
end
-
-
# @private
-
1
def method_name
-
@method_name ||= [
-
self.class.name.underscore,
-
RSpec.current_example.description.underscore
-
].join("_").tr(CHARS_TO_TRANSLATE.join, "_")[0...200] + "_#{rand(1000)}"
-
end
-
-
# Delegates to `Rails.application`.
-
1
def app
-
::Rails.application
-
end
-
-
1
included do |other|
-
ActiveSupport.on_load(:action_dispatch_system_test_case) do
-
ActionDispatch::SystemTesting::Server.silence_puma = true
-
end
-
-
begin
-
require 'capybara'
-
require 'action_dispatch/system_test_case'
-
rescue LoadError => e
-
abort """
-
LoadError: #{e.message}
-
System test integration requires Rails >= 5.1 and has a hard
-
dependency on a webserver and `capybara`, please add capybara to
-
your Gemfile and configure a webserver (e.g. `Capybara.server =
-
:webrick`) before attempting to use system specs.
-
""".gsub(/\s+/, ' ').strip
-
end
-
-
if ::Rails::VERSION::STRING >= '6.0'
-
original_before_teardown =
-
::ActionDispatch::SystemTesting::TestHelpers::SetupAndTeardown.instance_method(:before_teardown)
-
end
-
-
original_after_teardown =
-
::ActionDispatch::SystemTesting::TestHelpers::SetupAndTeardown.instance_method(:after_teardown)
-
-
other.include ::ActionDispatch::SystemTesting::TestHelpers::SetupAndTeardown
-
other.include ::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelper
-
other.include BlowAwayTeardownHooks
-
-
attr_reader :driver
-
-
if ActionDispatch::SystemTesting::Server.respond_to?(:silence_puma=)
-
ActionDispatch::SystemTesting::Server.silence_puma = true
-
end
-
-
def initialize(*args, &blk)
-
super(*args, &blk)
-
@driver = nil
-
-
self.class.before do
-
# A user may have already set the driver, so only default if driver
-
# is not set
-
driven_by(:selenium) unless @driver
-
end
-
end
-
-
def driven_by(driver, **driver_options, &blk)
-
@driver = ::ActionDispatch::SystemTestCase.driven_by(driver, **driver_options, &blk).tap(&:use)
-
end
-
-
before do
-
@routes = ::Rails.application.routes
-
end
-
-
after do
-
orig_stdout = $stdout
-
$stdout = StringIO.new
-
begin
-
if ::Rails::VERSION::STRING >= '6.0'
-
original_before_teardown.bind(self).call
-
end
-
original_after_teardown.bind(self).call
-
ensure
-
myio = $stdout
-
myio.rewind
-
RSpec.current_example.metadata[:extra_failure_lines] = myio.readlines
-
$stdout = orig_stdout
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'rspec/rails/view_assigns'
-
1
require 'rspec/rails/view_spec_methods'
-
1
require 'rspec/rails/view_path_builder'
-
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container class for view spec functionality.
-
1
module ViewExampleGroup
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::RailsExampleGroup
-
1
include ActionView::TestCase::Behavior
-
1
include RSpec::Rails::ViewAssigns
-
1
include RSpec::Rails::Matchers::RenderTemplate
-
-
# @private
-
1
module StubResolverCache
-
1
def self.resolver_for(hash)
-
@resolvers ||= {}
-
@resolvers[hash] ||= ActionView::FixtureResolver.new(hash)
-
end
-
end
-
-
# @private
-
1
module ClassMethods
-
1
def _default_helper
-
base = metadata[:description].split('/')[0..-2].join('/')
-
(base.camelize + 'Helper').constantize unless base.to_s.empty?
-
rescue NameError
-
nil
-
end
-
-
1
def _default_helpers
-
helpers = [_default_helper].compact
-
helpers << ApplicationHelper if Object.const_defined?('ApplicationHelper')
-
helpers
-
end
-
end
-
-
# DSL exposed to view specs.
-
1
module ExampleMethods
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
include ::Rails.application.routes.url_helpers
-
include ::Rails.application.routes.mounted_helpers
-
end
-
-
# @overload render
-
# @overload render({partial: path_to_file})
-
# @overload render({partial: path_to_file}, {... locals ...})
-
# @overload render({partial: path_to_file}, {... locals ...}) do ... end
-
#
-
# Delegates to ActionView::Base#render, so see documentation on that
-
# for more info.
-
#
-
# The only addition is that you can call render with no arguments, and
-
# RSpec will pass the top level description to render:
-
#
-
# describe "widgets/new.html.erb" do
-
# it "shows all the widgets" do
-
# render # => view.render(file: "widgets/new.html.erb")
-
# # ...
-
# end
-
# end
-
1
def render(options = {}, local_assigns = {}, &block)
-
options = _default_render_options if Hash === options && options.empty?
-
super(options, local_assigns, &block)
-
end
-
-
# The instance of `ActionView::Base` that is used to render the template.
-
# Use this to stub methods _before_ calling `render`.
-
#
-
# describe "widgets/new.html.erb" do
-
# it "shows all the widgets" do
-
# view.stub(:foo) { "foo" }
-
# render
-
# # ...
-
# end
-
# end
-
1
def view
-
_view
-
end
-
-
# Simulates the presence of a template on the file system by adding a
-
# Rails' FixtureResolver to the front of the view_paths list. Designed to
-
# help isolate view examples from partials rendered by the view template
-
# that is the subject of the example.
-
#
-
# stub_template("widgets/_widget.html.erb" => "This content.")
-
1
def stub_template(hash)
-
view.view_paths.unshift(StubResolverCache.resolver_for(hash))
-
end
-
-
# Provides access to the params hash that will be available within the
-
# view.
-
#
-
# params[:foo] = 'bar'
-
1
def params
-
controller.params
-
end
-
-
# @deprecated Use `view` instead.
-
1
def template
-
RSpec.deprecate("template", replacement: "view")
-
view
-
end
-
-
# @deprecated Use `rendered` instead.
-
1
def response
-
# `assert_template` expects `response` to implement a #body method
-
# like an `ActionDispatch::Response` does to force the view to
-
# render. For backwards compatibility, we use #response as an alias
-
# for #rendered, but it needs to implement #body to avoid
-
# `assert_template` raising a `NoMethodError`.
-
unless rendered.respond_to?(:body)
-
def rendered.body
-
self
-
end
-
end
-
-
rendered
-
end
-
-
1
private
-
-
1
def _default_render_options
-
formats = if ActionView::Template::Types.respond_to?(:symbols)
-
ActionView::Template::Types.symbols
-
else
-
[:html, :text, :js, :css, :xml, :json].map(&:to_s)
-
end.map { |x| Regexp.escape(x) }.join("|")
-
-
handlers = ActionView::Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
-
locales = "[a-z]{2}(?:-[A-Z]{2})?"
-
variants = "[^.]*"
-
path_regex = %r{
-
\A
-
(?<template>.*?)
-
(?:\.(?<locale>#{locales}))??
-
(?:\.(?<format>#{formats}))??
-
(?:\+(?<variant>#{variants}))??
-
(?:\.(?<handler>#{handlers}))?
-
\z
-
}x
-
-
# This regex should always find a match.
-
# Worst case, everything will be nil, and :template will just be
-
# the original string.
-
match = path_regex.match(_default_file_to_render)
-
-
render_options = {template: match[:template]}
-
render_options[:handlers] = [match[:handler]] if match[:handler]
-
render_options[:formats] = [match[:format].to_sym] if match[:format]
-
render_options[:locales] = [match[:locale]] if match[:locale]
-
render_options[:variants] = [match[:variant]] if match[:variant]
-
-
render_options
-
end
-
-
1
def _path_parts
-
_default_file_to_render.split("/")
-
end
-
-
1
def _controller_path
-
_path_parts[0..-2].join("/")
-
end
-
-
1
def _inferred_action
-
_path_parts.last.split(".").first
-
end
-
-
1
def _include_controller_helpers
-
helpers = controller._helpers
-
view.singleton_class.class_exec do
-
include helpers unless included_modules.include?(helpers)
-
end
-
end
-
end
-
-
1
included do
-
include ExampleMethods
-
-
helper(*_default_helpers)
-
-
before do
-
_include_controller_helpers
-
view.lookup_context.prefixes << _controller_path
-
-
controller.controller_path = _controller_path
-
-
path_params_to_merge = {}
-
path_params_to_merge[:controller] = _controller_path
-
path_params_to_merge[:action] = _inferred_action unless _inferred_action =~ /^_/
-
-
path_params = controller.request.path_parameters
-
-
controller.request.path_parameters = path_params.reverse_merge(path_params_to_merge)
-
controller.request.path = ViewPathBuilder.new(::Rails.application.routes).path_for(controller.request.path_parameters)
-
ViewSpecMethods.add_to(::ActionView::TestCase::TestController)
-
end
-
-
after do
-
ViewSpecMethods.remove_from(::ActionView::TestCase::TestController)
-
end
-
-
let(:_default_file_to_render) do |example|
-
example.example_group.top_level_description
-
end
-
end
-
end
-
end
-
end
-
1
require 'rspec/rails/extensions/active_record/proxy'
-
1
RSpec.configure do |rspec|
-
# Delay this in order to give users a chance to configure `expect_with`...
-
1
rspec.before(:suite) do
-
1
if defined?(RSpec::Matchers) &&
-
RSpec::Matchers.configuration.respond_to?(:syntax) && # RSpec 4 dropped support for monkey-patching `should` syntax
-
RSpec::Matchers.configuration.syntax.include?(:should) &&
-
defined?(ActiveRecord::Associations)
-
1
RSpec::Matchers.configuration.add_should_and_should_not_to ActiveRecord::Associations::CollectionProxy
-
end
-
end
-
end
-
1
if ::Rails::VERSION::STRING > '5'
-
1
require 'active_support/testing/file_fixtures'
-
-
1
module RSpec
-
1
module Rails
-
# @private
-
1
module FileFixtureSupport
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Testing::FileFixtures
-
-
1
included do
-
1
self.file_fixture_path = RSpec.configuration.file_fixture_path
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @private
-
1
module FixtureFileUploadSupport
-
1
delegate :fixture_file_upload, to: :rails_fixture_file_wrapper
-
-
1
private
-
-
1
def rails_fixture_file_wrapper
-
RailsFixtureFileWrapper.fixture_path = nil
-
resolved_fixture_path =
-
if respond_to?(:fixture_path) && !fixture_path.nil?
-
fixture_path.to_s
-
else
-
(RSpec.configuration.fixture_path || '').to_s
-
end
-
RailsFixtureFileWrapper.fixture_path = File.join(resolved_fixture_path, '') unless resolved_fixture_path.strip.empty?
-
RailsFixtureFileWrapper.instance
-
end
-
-
1
class RailsFixtureFileWrapper
-
1
include ActionDispatch::TestProcess if defined?(ActionDispatch::TestProcess)
-
-
1
class << self
-
1
attr_accessor :fixture_path
-
-
# Get instance of wrapper
-
1
def instance
-
@instance ||= new
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# @private
-
1
module FixtureSupport
-
1
if defined?(ActiveRecord::TestFixtures)
-
1
extend ActiveSupport::Concern
-
1
include RSpec::Rails::SetupAndTeardownAdapter
-
1
include RSpec::Rails::MinitestLifecycleAdapter
-
1
include RSpec::Rails::MinitestAssertionAdapter
-
1
include ActiveRecord::TestFixtures
-
-
1
included do
-
1
if RSpec.configuration.use_active_record?
-
1
include Fixtures
-
-
1
self.fixture_path = RSpec.configuration.fixture_path
-
1
if ::Rails::VERSION::STRING > '5'
-
1
self.use_transactional_tests = RSpec.configuration.use_transactional_fixtures
-
else
-
self.use_transactional_fixtures = RSpec.configuration.use_transactional_fixtures
-
end
-
1
self.use_instantiated_fixtures = RSpec.configuration.use_instantiated_fixtures
-
-
1
fixtures RSpec.configuration.global_fixtures if RSpec.configuration.global_fixtures
-
end
-
end
-
-
1
module Fixtures
-
1
extend ActiveSupport::Concern
-
-
1
class_methods do
-
1
def fixtures(*args)
-
orig_methods = private_instance_methods
-
super.tap do
-
new_methods = private_instance_methods - orig_methods
-
new_methods.each do |method_name|
-
proxy_method_warning_if_called_in_before_context_scope(method_name)
-
end
-
end
-
end
-
-
1
def proxy_method_warning_if_called_in_before_context_scope(method_name)
-
orig_implementation = instance_method(method_name)
-
define_method(method_name) do |*args, &blk|
-
if inspect.include?("before(:context)")
-
RSpec.warn_with("Calling fixture method in before :context ")
-
else
-
orig_implementation.bind(self).call(*args, &blk)
-
end
-
end
-
end
-
end
-
-
1
if ::Rails.version.to_f >= 6.1
-
# @private return the example name for TestFixtures
-
1
def name
-
4
@example
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'rspec/core/warnings'
-
1
require 'rspec/expectations'
-
1
require 'rspec/rails/feature_check'
-
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Container module for Rails specific matchers.
-
1
module Matchers
-
end
-
end
-
end
-
-
1
require 'rspec/rails/matchers/base_matcher'
-
1
require 'rspec/rails/matchers/have_rendered'
-
1
require 'rspec/rails/matchers/redirect_to'
-
1
require 'rspec/rails/matchers/routing_matchers'
-
1
require 'rspec/rails/matchers/be_new_record'
-
1
require 'rspec/rails/matchers/be_a_new'
-
1
require 'rspec/rails/matchers/relation_match_array'
-
1
require 'rspec/rails/matchers/be_valid'
-
1
require 'rspec/rails/matchers/have_http_status'
-
-
1
if RSpec::Rails::FeatureCheck.has_active_job?
-
1
require 'rspec/rails/matchers/active_job'
-
1
require 'rspec/rails/matchers/have_enqueued_mail'
-
end
-
-
1
if RSpec::Rails::FeatureCheck.has_action_cable_testing?
-
1
require 'rspec/rails/matchers/action_cable'
-
end
-
-
1
if RSpec::Rails::FeatureCheck.has_action_mailbox?
-
1
require 'rspec/rails/matchers/action_mailbox'
-
end
-
1
require "rspec/rails/matchers/action_cable/have_broadcasted_to"
-
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Namespace for various implementations of ActionCable features
-
#
-
# @api private
-
1
module ActionCable
-
end
-
-
# @api public
-
# Passes if a message has been sent to a stream/object inside a block.
-
# May chain `at_least`, `at_most` or `exactly` to specify a number of times.
-
# To specify channel from which message has been broadcasted to object use `from_channel`.
-
#
-
#
-
# @example
-
# expect {
-
# ActionCable.server.broadcast "messages", text: 'Hi!'
-
# }.to have_broadcasted_to("messages")
-
#
-
# expect {
-
# SomeChannel.broadcast_to(user)
-
# }.to have_broadcasted_to(user).from_channel(SomeChannel)
-
#
-
# # Using alias
-
# expect {
-
# ActionCable.server.broadcast "messages", text: 'Hi!'
-
# }.to broadcast_to("messages")
-
#
-
# expect {
-
# ActionCable.server.broadcast "messages", text: 'Hi!'
-
# ActionCable.server.broadcast "all", text: 'Hi!'
-
# }.to have_broadcasted_to("messages").exactly(:once)
-
#
-
# expect {
-
# 3.times { ActionCable.server.broadcast "messages", text: 'Hi!' }
-
# }.to have_broadcasted_to("messages").at_least(2).times
-
#
-
# expect {
-
# ActionCable.server.broadcast "messages", text: 'Hi!'
-
# }.to have_broadcasted_to("messages").at_most(:twice)
-
#
-
# expect {
-
# ActionCable.server.broadcast "messages", text: 'Hi!'
-
# }.to have_broadcasted_to("messages").with(text: 'Hi!')
-
1
def have_broadcasted_to(target = nil)
-
check_action_cable_adapter
-
-
ActionCable::HaveBroadcastedTo.new(target, channel: described_class)
-
end
-
1
alias_method :broadcast_to, :have_broadcasted_to
-
-
1
private
-
-
# @private
-
1
def check_action_cable_adapter
-
return if ::ActionCable::SubscriptionAdapter::Test === ::ActionCable.server.pubsub
-
-
raise StandardError, "To use ActionCable matchers set `adapter: test` in your cable.yml"
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
1
module ActionCable
-
# rubocop: disable Metrics/ClassLength
-
# @private
-
1
class HaveBroadcastedTo < RSpec::Matchers::BuiltIn::BaseMatcher
-
1
def initialize(target, channel:)
-
@target = target
-
@channel = channel
-
@block = proc { }
-
@data = nil
-
set_expected_number(:exactly, 1)
-
end
-
-
1
def with(data = nil, &block)
-
@data = data
-
@data = @data.with_indifferent_access if @data.is_a?(Hash)
-
@block = block if block
-
self
-
end
-
-
1
def exactly(count)
-
set_expected_number(:exactly, count)
-
self
-
end
-
-
1
def at_least(count)
-
set_expected_number(:at_least, count)
-
self
-
end
-
-
1
def at_most(count)
-
set_expected_number(:at_most, count)
-
self
-
end
-
-
1
def times
-
self
-
end
-
-
1
def once
-
exactly(:once)
-
end
-
-
1
def twice
-
exactly(:twice)
-
end
-
-
1
def thrice
-
exactly(:thrice)
-
end
-
-
1
def failure_message
-
"expected to broadcast #{base_message}".tap do |msg|
-
if @unmatching_msgs.any?
-
msg << "\nBroadcasted messages to #{stream}:"
-
@unmatching_msgs.each do |data|
-
msg << "\n #{data}"
-
end
-
end
-
end
-
end
-
-
1
def failure_message_when_negated
-
"expected not to broadcast #{base_message}"
-
end
-
-
1
def message_expectation_modifier
-
case @expectation_type
-
when :exactly then "exactly"
-
when :at_most then "at most"
-
when :at_least then "at least"
-
end
-
end
-
-
1
def supports_block_expectations?
-
true
-
end
-
-
1
def matches?(proc)
-
raise ArgumentError, "have_broadcasted_to and broadcast_to only support block expectations" unless Proc === proc
-
-
original_sent_messages_count = pubsub_adapter.broadcasts(stream).size
-
proc.call
-
in_block_messages = pubsub_adapter.broadcasts(stream).drop(original_sent_messages_count)
-
-
check(in_block_messages)
-
end
-
-
1
def from_channel(channel)
-
@channel = channel
-
self
-
end
-
-
1
private
-
-
1
def stream
-
@stream ||= if @target.is_a?(String)
-
@target
-
else
-
check_channel_presence
-
@channel.broadcasting_for(@target)
-
end
-
end
-
-
1
def check(messages)
-
@matching_msgs, @unmatching_msgs = messages.partition do |msg|
-
decoded = ActiveSupport::JSON.decode(msg)
-
decoded = decoded.with_indifferent_access if decoded.is_a?(Hash)
-
-
if @data.nil? || @data === decoded
-
@block.call(decoded)
-
true
-
else
-
false
-
end
-
end
-
-
@matching_msgs_count = @matching_msgs.size
-
-
case @expectation_type
-
when :exactly then @expected_number == @matching_msgs_count
-
when :at_most then @expected_number >= @matching_msgs_count
-
when :at_least then @expected_number <= @matching_msgs_count
-
end
-
end
-
-
1
def set_expected_number(relativity, count)
-
@expectation_type = relativity
-
@expected_number =
-
case count
-
when :once then 1
-
when :twice then 2
-
when :thrice then 3
-
else Integer(count)
-
end
-
end
-
-
1
def base_message
-
"#{message_expectation_modifier} #{@expected_number} messages to #{stream}".tap do |msg|
-
msg << " with #{data_description(@data)}" unless @data.nil?
-
msg << ", but broadcast #{@matching_msgs_count}"
-
end
-
end
-
-
1
def data_description(data)
-
if data.is_a?(RSpec::Matchers::Composable)
-
data.description
-
else
-
data
-
end
-
end
-
-
1
def pubsub_adapter
-
::ActionCable.server.pubsub
-
end
-
-
1
def check_channel_presence
-
return if @channel.present? && @channel.respond_to?(:channel_name)
-
-
error_msg = "Broadcasting channel can't be infered. Please, specify it with `from_channel`"
-
raise ArgumentError, error_msg
-
end
-
end
-
# rubocop: enable Metrics/ClassLength
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
1
module ActionCable
-
# @api private
-
# Provides the implementation for `have_stream`, `have_stream_for`, and `have_stream_from`.
-
# Not intended to be instantiated directly.
-
1
class HaveStream < RSpec::Matchers::BuiltIn::BaseMatcher
-
# @api private
-
# @return [String]
-
1
def failure_message
-
"expected to have #{base_message}"
-
end
-
-
# @api private
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected not to have #{base_message}"
-
end
-
-
# @api private
-
# @return [Boolean]
-
1
def matches?(subscription)
-
raise(ArgumentError, "have_streams is used for negated expectations only") if no_expected?
-
-
match(subscription)
-
end
-
-
# @api private
-
# @return [Boolean]
-
1
def does_not_match?(subscription)
-
!match(subscription)
-
end
-
-
1
private
-
-
1
def match(subscription)
-
case subscription
-
when ::ActionCable::Channel::Base
-
@actual = subscription.streams
-
no_expected? ? actual.any? : actual.any? { |i| expected === i }
-
else
-
raise ArgumentError, "have_stream, have_stream_from and have_stream_from support expectations on subscription only"
-
end
-
end
-
-
1
def base_message
-
no_expected? ? "any stream started" : "stream #{expected_formatted} started, but have #{actual_formatted}"
-
end
-
-
1
def no_expected?
-
!defined?(@expected)
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Namespace for various implementations of ActionMailbox features
-
#
-
# @api private
-
1
module ActionMailbox
-
# @private
-
1
class Base < RSpec::Rails::Matchers::BaseMatcher
-
1
private
-
-
1
def create_inbound_email(message)
-
RSpec::Rails::MailboxExampleGroup.create_inbound_email(message)
-
end
-
end
-
-
# @private
-
1
class ReceiveInboundEmail < Base
-
1
def initialize(message)
-
super()
-
-
@inbound_email = create_inbound_email(message)
-
end
-
-
1
def matches?(mailbox)
-
@mailbox = mailbox
-
@receiver = ApplicationMailbox.router.send(:match_to_mailbox, inbound_email)
-
-
@receiver == @mailbox
-
end
-
-
1
def failure_message
-
"expected #{describe_inbound_email} to route to #{mailbox}".tap do |msg|
-
if receiver
-
msg << ", but routed to #{receiver} instead"
-
end
-
end
-
end
-
-
1
def failure_message_when_negated
-
"expected #{describe_inbound_email} not to route to #{mailbox}"
-
end
-
-
1
private
-
-
1
attr_reader :inbound_email, :mailbox, :receiver
-
-
1
def describe_inbound_email
-
"mail to #{inbound_email.mail.to.to_sentence}"
-
end
-
end
-
end
-
-
# @api public
-
# Passes if the given inbound email would be routed to the subject inbox.
-
#
-
# @param message [Hash, Mail::Message] a mail message or hash of
-
# attributes used to build one
-
1
def receive_inbound_email(message)
-
ActionMailbox::ReceiveInboundEmail.new(message)
-
end
-
end
-
end
-
end
-
1
require "active_job/base"
-
1
require "active_job/arguments"
-
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Namespace for various implementations of ActiveJob features
-
#
-
# @api private
-
1
module ActiveJob
-
# rubocop: disable Metrics/ClassLength
-
# @private
-
1
class Base < RSpec::Rails::Matchers::BaseMatcher
-
1
def initialize
-
@args = []
-
@queue = nil
-
@at = nil
-
@block = proc { }
-
set_expected_number(:exactly, 1)
-
end
-
-
1
def with(*args, &block)
-
@args = args
-
@block = block if block.present?
-
self
-
end
-
-
1
def on_queue(queue)
-
@queue = queue.to_s
-
self
-
end
-
-
1
def at(time_or_date)
-
case time_or_date
-
when Time then @at = Time.at(time_or_date.to_f)
-
else
-
@at = time_or_date
-
end
-
self
-
end
-
-
1
def exactly(count)
-
set_expected_number(:exactly, count)
-
self
-
end
-
-
1
def at_least(count)
-
set_expected_number(:at_least, count)
-
self
-
end
-
-
1
def at_most(count)
-
set_expected_number(:at_most, count)
-
self
-
end
-
-
1
def times
-
self
-
end
-
-
1
def once
-
exactly(:once)
-
end
-
-
1
def twice
-
exactly(:twice)
-
end
-
-
1
def thrice
-
exactly(:thrice)
-
end
-
-
1
def failure_message
-
"expected to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION} #{base_message}".tap do |msg|
-
if @unmatching_jobs.any?
-
msg << "\nQueued jobs:"
-
@unmatching_jobs.each do |job|
-
msg << "\n #{base_job_message(job)}"
-
end
-
end
-
end
-
end
-
-
1
def failure_message_when_negated
-
"expected not to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION} #{base_message}"
-
end
-
-
1
def message_expectation_modifier
-
case @expectation_type
-
when :exactly then "exactly"
-
when :at_most then "at most"
-
when :at_least then "at least"
-
end
-
end
-
-
1
def supports_block_expectations?
-
true
-
end
-
-
1
private
-
-
1
def check(jobs)
-
@matching_jobs, @unmatching_jobs = jobs.partition do |job|
-
if job_match?(job) && arguments_match?(job) && queue_match?(job) && at_match?(job)
-
args = deserialize_arguments(job)
-
@block.call(*args)
-
true
-
else
-
false
-
end
-
end
-
@matching_jobs_count = @matching_jobs.size
-
-
case @expectation_type
-
when :exactly then @expected_number == @matching_jobs_count
-
when :at_most then @expected_number >= @matching_jobs_count
-
when :at_least then @expected_number <= @matching_jobs_count
-
end
-
end
-
-
1
def base_message
-
"#{message_expectation_modifier} #{@expected_number} jobs,".tap do |msg|
-
msg << " with #{@args}," if @args.any?
-
msg << " on queue #{@queue}," if @queue
-
msg << " at #{@at.inspect}," if @at
-
msg << " but #{self.class::MESSAGE_EXPECTATION_ACTION} #{@matching_jobs_count}"
-
end
-
end
-
-
1
def base_job_message(job)
-
msg_parts = []
-
msg_parts << "with #{deserialize_arguments(job)}" if job[:args].any?
-
msg_parts << "on queue #{job[:queue]}" if job[:queue]
-
msg_parts << "at #{Time.at(job[:at])}" if job[:at]
-
-
"#{job[:job].name} job".tap do |msg|
-
msg << " #{msg_parts.join(', ')}" if msg_parts.any?
-
end
-
end
-
-
1
def job_match?(job)
-
@job ? @job == job[:job] : true
-
end
-
-
1
def arguments_match?(job)
-
if @args.any?
-
args = serialize_and_deserialize_arguments(@args)
-
deserialized_args = deserialize_arguments(job)
-
RSpec::Mocks::ArgumentListMatcher.new(*args).args_match?(*deserialized_args)
-
else
-
true
-
end
-
end
-
-
1
def queue_match?(job)
-
return true unless @queue
-
-
@queue == job[:queue]
-
end
-
-
1
def at_match?(job)
-
return true unless @at
-
return job[:at].nil? if @at == :no_wait
-
return false unless job[:at]
-
-
values_match?(@at, Time.at(job[:at]))
-
end
-
-
1
def set_expected_number(relativity, count)
-
@expectation_type = relativity
-
@expected_number = case count
-
when :once then 1
-
when :twice then 2
-
when :thrice then 3
-
else Integer(count)
-
end
-
end
-
-
1
def serialize_and_deserialize_arguments(args)
-
serialized = ::ActiveJob::Arguments.serialize(args)
-
::ActiveJob::Arguments.deserialize(serialized)
-
rescue ::ActiveJob::SerializationError
-
args
-
end
-
-
1
def deserialize_arguments(job)
-
::ActiveJob::Arguments.deserialize(job[:args])
-
rescue ::ActiveJob::DeserializationError
-
job[:args]
-
end
-
-
1
def queue_adapter
-
::ActiveJob::Base.queue_adapter
-
end
-
end
-
# rubocop: enable Metrics/ClassLength
-
-
# @private
-
1
class HaveEnqueuedJob < Base
-
1
FAILURE_MESSAGE_EXPECTATION_ACTION = 'enqueue'.freeze
-
1
MESSAGE_EXPECTATION_ACTION = 'enqueued'.freeze
-
-
1
def initialize(job)
-
super()
-
@job = job
-
end
-
-
1
def matches?(proc)
-
raise ArgumentError, "have_enqueued_job and enqueue_job only support block expectations" unless Proc === proc
-
-
original_enqueued_jobs_count = queue_adapter.enqueued_jobs.count
-
proc.call
-
in_block_jobs = queue_adapter.enqueued_jobs.drop(original_enqueued_jobs_count)
-
-
check(in_block_jobs)
-
end
-
-
1
def does_not_match?(proc)
-
set_expected_number(:at_least, 1)
-
-
!matches?(proc)
-
end
-
end
-
-
# @private
-
1
class HaveBeenEnqueued < Base
-
1
FAILURE_MESSAGE_EXPECTATION_ACTION = 'enqueue'.freeze
-
1
MESSAGE_EXPECTATION_ACTION = 'enqueued'.freeze
-
-
1
def matches?(job)
-
@job = job
-
check(queue_adapter.enqueued_jobs)
-
end
-
-
1
def does_not_match?(proc)
-
set_expected_number(:at_least, 1)
-
-
!matches?(proc)
-
end
-
end
-
-
# @private
-
1
class HavePerformedJob < Base
-
1
FAILURE_MESSAGE_EXPECTATION_ACTION = 'perform'.freeze
-
1
MESSAGE_EXPECTATION_ACTION = 'performed'.freeze
-
-
1
def initialize(job)
-
super()
-
@job = job
-
end
-
-
1
def matches?(proc)
-
raise ArgumentError, "have_performed_job only supports block expectations" unless Proc === proc
-
-
original_performed_jobs_count = queue_adapter.performed_jobs.count
-
proc.call
-
in_block_jobs = queue_adapter.performed_jobs.drop(original_performed_jobs_count)
-
-
check(in_block_jobs)
-
end
-
end
-
-
# @private
-
1
class HaveBeenPerformed < Base
-
1
FAILURE_MESSAGE_EXPECTATION_ACTION = 'perform'.freeze
-
1
MESSAGE_EXPECTATION_ACTION = 'performed'.freeze
-
-
1
def matches?(job)
-
@job = job
-
check(queue_adapter.performed_jobs)
-
end
-
end
-
end
-
-
# @api public
-
# Passes if a job has been enqueued inside block. May chain at_least, at_most or exactly to specify a number of times.
-
#
-
# @example
-
# expect {
-
# HeavyLiftingJob.perform_later
-
# }.to have_enqueued_job
-
#
-
# # Using alias
-
# expect {
-
# HeavyLiftingJob.perform_later
-
# }.to enqueue_job
-
#
-
# expect {
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# }.to have_enqueued_job(HelloJob).exactly(:once)
-
#
-
# expect {
-
# 3.times { HelloJob.perform_later }
-
# }.to have_enqueued_job(HelloJob).at_least(2).times
-
#
-
# expect {
-
# HelloJob.perform_later
-
# }.to have_enqueued_job(HelloJob).at_most(:twice)
-
#
-
# expect {
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# }.to have_enqueued_job(HelloJob).and have_enqueued_job(HeavyLiftingJob)
-
#
-
# expect {
-
# HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
-
# }.to have_enqueued_job.with(42).on_queue("low").at(Date.tomorrow.noon)
-
#
-
# expect {
-
# HelloJob.set(queue: "low").perform_later(42)
-
# }.to have_enqueued_job.with(42).on_queue("low").at(:no_wait)
-
#
-
# expect {
-
# HelloJob.perform_later('rspec_rails', 'rails', 42)
-
# }.to have_enqueued_job.with { |from, to, times|
-
# # Perform more complex argument matching using dynamic arguments
-
# expect(from).to include "_#{to}"
-
# }
-
1
def have_enqueued_job(job = nil)
-
check_active_job_adapter
-
ActiveJob::HaveEnqueuedJob.new(job)
-
end
-
1
alias_method :enqueue_job, :have_enqueued_job
-
-
# @api public
-
# Passes if a job has been enqueued. May chain at_least, at_most or exactly to specify a number of times.
-
#
-
# @example
-
# before { ActiveJob::Base.queue_adapter.enqueued_jobs.clear }
-
#
-
# HeavyLiftingJob.perform_later
-
# expect(HeavyLiftingJob).to have_been_enqueued
-
#
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# expect(HeavyLiftingJob).to have_been_enqueued.exactly(:once)
-
#
-
# 3.times { HelloJob.perform_later }
-
# expect(HelloJob).to have_been_enqueued.at_least(2).times
-
#
-
# HelloJob.perform_later
-
# expect(HelloJob).to enqueue_job(HelloJob).at_most(:twice)
-
#
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# expect(HelloJob).to have_been_enqueued
-
# expect(HeavyLiftingJob).to have_been_enqueued
-
#
-
# HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
-
# expect(HelloJob).to have_been_enqueued.with(42).on_queue("low").at(Date.tomorrow.noon)
-
#
-
# HelloJob.set(queue: "low").perform_later(42)
-
# expect(HelloJob).to have_been_enqueued.with(42).on_queue("low").at(:no_wait)
-
1
def have_been_enqueued
-
check_active_job_adapter
-
ActiveJob::HaveBeenEnqueued.new
-
end
-
-
# @api public
-
# Passes if a job has been performed inside block. May chain at_least, at_most or exactly to specify a number of times.
-
#
-
# @example
-
# expect {
-
# perform_jobs { HeavyLiftingJob.perform_later }
-
# }.to have_performed_job
-
#
-
# expect {
-
# perform_jobs {
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# }
-
# }.to have_performed_job(HelloJob).exactly(:once)
-
#
-
# expect {
-
# perform_jobs { 3.times { HelloJob.perform_later } }
-
# }.to have_performed_job(HelloJob).at_least(2).times
-
#
-
# expect {
-
# perform_jobs { HelloJob.perform_later }
-
# }.to have_performed_job(HelloJob).at_most(:twice)
-
#
-
# expect {
-
# perform_jobs {
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# }
-
# }.to have_performed_job(HelloJob).and have_performed_job(HeavyLiftingJob)
-
#
-
# expect {
-
# perform_jobs {
-
# HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
-
# }
-
# }.to have_performed_job.with(42).on_queue("low").at(Date.tomorrow.noon)
-
1
def have_performed_job(job = nil)
-
check_active_job_adapter
-
ActiveJob::HavePerformedJob.new(job)
-
end
-
1
alias_method :perform_job, :have_performed_job
-
-
# @api public
-
# Passes if a job has been performed. May chain at_least, at_most or exactly to specify a number of times.
-
#
-
# @example
-
# before do
-
# ActiveJob::Base.queue_adapter.performed_jobs.clear
-
# ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
-
# ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
-
# end
-
#
-
# HeavyLiftingJob.perform_later
-
# expect(HeavyLiftingJob).to have_been_performed
-
#
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# expect(HeavyLiftingJob).to have_been_performed.exactly(:once)
-
#
-
# 3.times { HelloJob.perform_later }
-
# expect(HelloJob).to have_been_performed.at_least(2).times
-
#
-
# HelloJob.perform_later
-
# HeavyLiftingJob.perform_later
-
# expect(HelloJob).to have_been_performed
-
# expect(HeavyLiftingJob).to have_been_performed
-
#
-
# HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
-
# expect(HelloJob).to have_been_performed.with(42).on_queue("low").at(Date.tomorrow.noon)
-
1
def have_been_performed
-
check_active_job_adapter
-
ActiveJob::HaveBeenPerformed.new
-
end
-
-
1
private
-
-
# @private
-
1
def check_active_job_adapter
-
return if ::ActiveJob::QueueAdapters::TestAdapter === ::ActiveJob::Base.queue_adapter
-
-
raise StandardError, "To use ActiveJob matchers set `ActiveJob::Base.queue_adapter = :test`"
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# @api private
-
#
-
# Base class to build matchers. Should not be instantiated directly.
-
1
class BaseMatcher
-
1
include RSpec::Matchers::Composable
-
-
# @api private
-
# Used to detect when no arg is passed to `initialize`.
-
# `nil` cannot be used because it's a valid value to pass.
-
1
UNDEFINED = Object.new.freeze
-
-
# @private
-
1
attr_reader :actual, :expected, :rescued_exception
-
-
# @private
-
1
attr_writer :matcher_name
-
-
1
def initialize(expected = UNDEFINED)
-
@expected = expected unless UNDEFINED.equal?(expected)
-
end
-
-
# @api private
-
# Indicates if the match is successful. Delegates to `match`, which
-
# should be defined on a subclass. Takes care of consistently
-
# initializing the `actual` attribute.
-
1
def matches?(actual)
-
@actual = actual
-
match(expected, actual)
-
end
-
-
# @api private
-
# Used to wrap a block of code that will indicate failure by
-
# raising one of the named exceptions.
-
#
-
# This is used by rspec-rails for some of its matchers that
-
# wrap rails' assertions.
-
1
def match_unless_raises(*exceptions)
-
exceptions.unshift Exception if exceptions.empty?
-
begin
-
yield
-
true
-
rescue *exceptions => @rescued_exception
-
false
-
end
-
end
-
-
# @api private
-
# Generates a description using {RSpec::Matchers::EnglishPhrasing}.
-
# @return [String]
-
1
def description
-
desc = RSpec::Matchers::EnglishPhrasing.split_words(self.class.matcher_name)
-
desc << RSpec::Matchers::EnglishPhrasing.list(@expected) if defined?(@expected)
-
desc
-
end
-
-
# @api private
-
# Matchers are not diffable by default. Override this to make your
-
# subclass diffable.
-
1
def diffable?
-
false
-
end
-
-
# @api private
-
# Most matchers are value matchers (i.e. meant to work with `expect(value)`)
-
# rather than block matchers (i.e. meant to work with `expect { }`), so
-
# this defaults to false. Block matchers must override this to return true.
-
1
def supports_block_expectations?
-
false
-
end
-
-
# @api private
-
1
def expects_call_stack_jump?
-
false
-
end
-
-
# @private
-
1
def expected_formatted
-
RSpec::Support::ObjectFormatter.format(@expected)
-
end
-
-
# @private
-
1
def actual_formatted
-
RSpec::Support::ObjectFormatter.format(@actual)
-
end
-
-
# @private
-
1
def self.matcher_name
-
@matcher_name ||= underscore(name.split('::').last)
-
end
-
-
# @private
-
1
def matcher_name
-
if defined?(@matcher_name)
-
@matcher_name
-
else
-
self.class.matcher_name
-
end
-
end
-
-
# @private
-
# Borrowed from ActiveSupport.
-
1
def self.underscore(camel_cased_word)
-
word = camel_cased_word.to_s.dup
-
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
-
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
-
word.tr!('-', '_')
-
word.downcase!
-
word
-
end
-
1
private_class_method :underscore
-
-
1
private
-
-
1
def assert_ivars(*expected_ivars)
-
return unless (expected_ivars - present_ivars).any?
-
-
ivar_list = RSpec::Matchers::EnglishPhrasing.list(expected_ivars)
-
raise "#{self.class.name} needs to supply#{ivar_list}"
-
end
-
-
1
alias present_ivars instance_variables
-
-
# @private
-
1
module HashFormatting
-
# `{ :a => 5, :b => 2 }.inspect` produces:
-
#
-
# {:a=>5, :b=>2}
-
#
-
# ...but it looks much better as:
-
#
-
# {:a => 5, :b => 2}
-
#
-
# This is idempotent and safe to run on a string multiple times.
-
1
def improve_hash_formatting(inspect_string)
-
inspect_string.gsub(/(\S)=>(\S)/, '\1 => \2')
-
end
-
1
module_function :improve_hash_formatting
-
end
-
-
1
include HashFormatting
-
-
# @api private
-
# Provides default implementations of failure messages, based on the `description`.
-
1
module DefaultFailureMessages
-
# @api private
-
# Provides a good generic failure message. Based on `description`.
-
# When subclassing, if you are not satisfied with this failure message
-
# you often only need to override `description`.
-
# @return [String]
-
1
def failure_message
-
"expected #{description_of @actual} to #{description}".dup
-
end
-
-
# @api private
-
# Provides a good generic negative failure message. Based on `description`.
-
# When subclassing, if you are not satisfied with this failure message
-
# you often only need to override `description`.
-
# @return [String]
-
1
def failure_message_when_negated
-
"expected #{description_of @actual} not to #{description}".dup
-
end
-
-
# @private
-
1
def self.has_default_failure_messages?(matcher)
-
matcher.method(:failure_message).owner == self &&
-
matcher.method(:failure_message_when_negated).owner == self
-
rescue NameError
-
false
-
end
-
end
-
-
1
include DefaultFailureMessages
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# @api private
-
#
-
# Matcher class for `be_a_new`. Should not be instantiated directly.
-
#
-
# @see RSpec::Rails::Matchers#be_a_new
-
1
class BeANew < RSpec::Rails::Matchers::BaseMatcher
-
# @private
-
1
def initialize(expected)
-
@expected = expected
-
end
-
-
# @private
-
1
def matches?(actual)
-
@actual = actual
-
actual.is_a?(expected) && actual.new_record? && attributes_match?(actual)
-
end
-
-
# @api public
-
# @see RSpec::Rails::Matchers#be_a_new
-
1
def with(expected_attributes)
-
attributes.merge!(expected_attributes)
-
self
-
end
-
-
# @private
-
1
def failure_message
-
[].tap do |message|
-
unless actual.is_a?(expected) && actual.new_record?
-
message << "expected #{actual.inspect} to be a new #{expected.inspect}"
-
end
-
unless attributes_match?(actual)
-
describe_unmatched_attributes = surface_descriptions_in(unmatched_attributes)
-
if unmatched_attributes.size > 1
-
message << "attributes #{describe_unmatched_attributes.inspect} were not set on #{actual.inspect}"
-
else
-
message << "attribute #{describe_unmatched_attributes.inspect} was not set on #{actual.inspect}"
-
end
-
end
-
end.join(' and ')
-
end
-
-
1
private
-
-
1
def attributes
-
@attributes ||= {}
-
end
-
-
1
def attributes_match?(actual)
-
attributes.stringify_keys.all? do |key, value|
-
values_match?(value, actual.attributes[key])
-
end
-
end
-
-
1
def unmatched_attributes
-
attributes.stringify_keys.reject do |key, value|
-
values_match?(value, actual.attributes[key])
-
end
-
end
-
end
-
-
# @api public
-
# Passes if actual is an instance of `model_class` and returns `true` for
-
# `new_record?`. Typically used to specify instance variables assigned to
-
# views by controller actions
-
#
-
# Use the `with` method to specify the specific attributes to match on the
-
# new record.
-
#
-
# @example
-
# get :new
-
# assigns(:thing).should be_a_new(Thing)
-
#
-
# post :create, :thing => { :name => "Illegal Value" }
-
# assigns(:thing).should be_a_new(Thing).with(:name => nil)
-
1
def be_a_new(model_class)
-
BeANew.new(model_class)
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# @private
-
1
class BeANewRecord < RSpec::Rails::Matchers::BaseMatcher
-
1
def matches?(actual)
-
actual.new_record?
-
end
-
-
1
def failure_message
-
"expected #{actual.inspect} to be a new record, but was persisted"
-
end
-
-
1
def failure_message_when_negated
-
"expected #{actual.inspect} to be persisted, but was a new record"
-
end
-
end
-
-
# @api public
-
# Passes if actual returns `true` for `new_record?`.
-
#
-
# @example
-
# get :new
-
# expect(assigns(:thing)).to be_new_record
-
1
def be_new_record
-
BeANewRecord.new
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# @private
-
1
class BeValid < RSpec::Matchers::BuiltIn::Be
-
1
def initialize(*args)
-
1
@args = args
-
end
-
-
1
def matches?(actual)
-
1
@actual = actual
-
1
actual.valid?(*@args)
-
end
-
-
1
def failure_message
-
message = "expected #{actual.inspect} to be valid"
-
-
if actual.respond_to?(:errors) && actual.method(:errors).arity < 1
-
errors = if actual.errors.respond_to?(:full_messages)
-
actual.errors.full_messages
-
else
-
actual.errors
-
end
-
-
message << ", but got errors: #{errors.map(&:to_s).join(', ')}"
-
end
-
-
message
-
end
-
-
1
def failure_message_when_negated
-
"expected #{actual.inspect} not to be valid"
-
end
-
end
-
-
# @api public
-
# Passes if the given model instance's `valid?` method is true, meaning
-
# all of the `ActiveModel::Validations` passed and no errors exist. If a
-
# message is not given, a default message is shown listing each error.
-
#
-
# @example
-
# thing = Thing.new
-
# expect(thing).to be_valid
-
1
def be_valid(*args)
-
1
BeValid.new(*args)
-
end
-
end
-
end
-
end
-
# We require the minimum amount of rspec-mocks possible to avoid
-
# conflicts with other mocking frameworks.
-
# See: https://github.com/rspec/rspec-rails/issues/2252
-
1
require "rspec/mocks/argument_matchers"
-
1
require "rspec/rails/matchers/active_job"
-
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Matcher class for `have_enqueued_mail`. Should not be instantiated directly.
-
#
-
# @private
-
# @see RSpec::Rails::Matchers#have_enqueued_mail
-
1
class HaveEnqueuedMail < ActiveJob::HaveEnqueuedJob
-
1
MAILER_JOB_METHOD = 'deliver_now'.freeze
-
-
1
include RSpec::Mocks::ArgumentMatchers
-
-
1
def initialize(mailer_class, method_name)
-
super(nil)
-
@mailer_class = mailer_class
-
@method_name = method_name
-
@mail_args = []
-
end
-
-
1
def description
-
"enqueues #{mailer_class_name}.#{@method_name}"
-
end
-
-
1
def with(*args, &block)
-
@mail_args = args
-
block.nil? ? super : super(&yield_mail_args(block))
-
end
-
-
1
def matches?(block)
-
raise ArgumentError, 'have_enqueued_mail and enqueue_mail only work with block arguments' unless block.respond_to?(:call)
-
-
check_active_job_adapter
-
super
-
end
-
-
1
def failure_message
-
"expected to enqueue #{base_message}".tap do |msg|
-
msg << "\n#{unmatching_mail_jobs_message}" if unmatching_mail_jobs.any?
-
end
-
end
-
-
1
def failure_message_when_negated
-
"expected not to enqueue #{base_message}"
-
end
-
-
1
private
-
-
1
def base_message
-
[mailer_class_name, @method_name].compact.join('.').tap do |msg|
-
msg << " #{expected_count_message}"
-
msg << " with #{@mail_args}," if @mail_args.any?
-
msg << " on queue #{@queue}," if @queue
-
msg << " at #{@at.inspect}," if @at
-
msg << " but enqueued #{@matching_jobs.size}"
-
end
-
end
-
-
1
def expected_count_message
-
"#{message_expectation_modifier} #{@expected_number} #{@expected_number == 1 ? 'time' : 'times'}"
-
end
-
-
1
def mailer_class_name
-
@mailer_class ? @mailer_class.name : 'ActionMailer::Base'
-
end
-
-
1
def job_match?(job)
-
legacy_mail?(job) || parameterized_mail?(job) || unified_mail?(job)
-
end
-
-
1
def arguments_match?(job)
-
@args =
-
if @mail_args.any?
-
base_mailer_args + @mail_args
-
elsif @mailer_class && @method_name
-
base_mailer_args + [any_args]
-
elsif @mailer_class
-
[mailer_class_name, any_args]
-
else
-
[]
-
end
-
-
super(job)
-
end
-
-
1
def base_mailer_args
-
[mailer_class_name, @method_name.to_s, MAILER_JOB_METHOD]
-
end
-
-
1
def yield_mail_args(block)
-
proc { |*job_args| block.call(*(job_args - base_mailer_args)) }
-
end
-
-
1
def check_active_job_adapter
-
return if ::ActiveJob::QueueAdapters::TestAdapter === ::ActiveJob::Base.queue_adapter
-
-
raise StandardError, "To use HaveEnqueuedMail matcher set `ActiveJob::Base.queue_adapter = :test`"
-
end
-
-
1
def unmatching_mail_jobs
-
@unmatching_jobs.select do |job|
-
job_match?(job)
-
end
-
end
-
-
1
def unmatching_mail_jobs_message
-
msg = "Queued deliveries:"
-
-
unmatching_mail_jobs.each do |job|
-
msg << "\n #{mail_job_message(job)}"
-
end
-
-
msg
-
end
-
-
1
def mail_job_message(job)
-
mailer_method = job[:args][0..1].join('.')
-
-
mailer_args = job[:args][3..-1]
-
msg_parts = []
-
msg_parts << "with #{mailer_args}" if mailer_args.any?
-
msg_parts << "on queue #{job[:queue]}" if job[:queue] && job[:queue] != 'mailers'
-
msg_parts << "at #{Time.at(job[:at])}" if job[:at]
-
-
"#{mailer_method} #{msg_parts.join(', ')}".strip
-
end
-
-
1
def legacy_mail?(job)
-
job[:job] <= ActionMailer::DeliveryJob
-
end
-
-
1
def parameterized_mail?(job)
-
RSpec::Rails::FeatureCheck.has_action_mailer_parameterized? && job[:job] <= ActionMailer::Parameterized::DeliveryJob
-
end
-
-
1
def unified_mail?(job)
-
RSpec::Rails::FeatureCheck.has_action_mailer_unified_delivery? && job[:job] <= ActionMailer::MailDeliveryJob
-
end
-
end
-
# @api public
-
# Passes if an email has been enqueued inside block.
-
# May chain with to specify expected arguments.
-
# May chain at_least, at_most or exactly to specify a number of times.
-
# May chain at to specify a send time.
-
# May chain on_queue to specify a queue.
-
#
-
# @example
-
# expect {
-
# MyMailer.welcome(user).deliver_later
-
# }.to have_enqueued_mail
-
#
-
# expect {
-
# MyMailer.welcome(user).deliver_later
-
# }.to have_enqueued_mail(MyMailer)
-
#
-
# expect {
-
# MyMailer.welcome(user).deliver_later
-
# }.to have_enqueued_mail(MyMailer, :welcome)
-
#
-
# # Using alias
-
# expect {
-
# MyMailer.welcome(user).deliver_later
-
# }.to enqueue_mail(MyMailer, :welcome)
-
#
-
# expect {
-
# MyMailer.welcome(user).deliver_later
-
# }.to have_enqueued_mail(MyMailer, :welcome).with(user)
-
#
-
# expect {
-
# MyMailer.welcome(user).deliver_later
-
# MyMailer.welcome(user).deliver_later
-
# }.to have_enqueued_mail(MyMailer, :welcome).at_least(:once)
-
#
-
# expect {
-
# MyMailer.welcome(user).deliver_later
-
# }.to have_enqueued_mail(MyMailer, :welcome).at_most(:twice)
-
#
-
# expect {
-
# MyMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
-
# }.to have_enqueued_mail(MyMailer, :welcome).at(Date.tomorrow.noon)
-
#
-
# expect {
-
# MyMailer.welcome(user).deliver_later(queue: :urgent_mail)
-
# }.to have_enqueued_mail(MyMailer, :welcome).on_queue(:urgent_mail)
-
1
def have_enqueued_mail(mailer_class = nil, mail_method_name = nil)
-
HaveEnqueuedMail.new(mailer_class, mail_method_name)
-
end
-
1
alias_method :have_enqueued_email, :have_enqueued_mail
-
1
alias_method :enqueue_mail, :have_enqueued_mail
-
1
alias_method :enqueue_email, :have_enqueued_mail
-
end
-
end
-
end
-
# The following code inspired and modified from Rails' `assert_response`:
-
#
-
# https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/assertions/response.rb#L22-L38
-
#
-
# Thank you to all the Rails devs who did the heavy lifting on this!
-
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Namespace for various implementations of `have_http_status`.
-
#
-
# @api private
-
1
module HaveHttpStatus
-
# Instantiates an instance of the proper matcher based on the provided
-
# `target`.
-
#
-
# @param target [Object] expected http status or code
-
# @return response matcher instance
-
1
def self.matcher_for_status(target)
-
if GenericStatus.valid_statuses.include?(target)
-
GenericStatus.new(target)
-
elsif Symbol === target
-
SymbolicStatus.new(target)
-
else
-
NumericCode.new(target)
-
end
-
end
-
-
# @api private
-
# Conversion function to coerce the provided object into an
-
# `ActionDispatch::TestResponse`.
-
#
-
# @param obj [Object] object to convert to a response
-
# @return [ActionDispatch::TestResponse]
-
1
def as_test_response(obj)
-
if ::ActionDispatch::Response === obj
-
::ActionDispatch::TestResponse.from_response(obj)
-
elsif ::ActionDispatch::TestResponse === obj
-
obj
-
elsif obj.respond_to?(:status_code) && obj.respond_to?(:response_headers)
-
# Acts As Capybara Session
-
# Hack to support `Capybara::Session` without having to load
-
# Capybara or catch `NameError`s for the undefined constants
-
obj = ActionDispatch::Response.new.tap do |resp|
-
resp.status = obj.status_code
-
resp.headers.clear
-
resp.headers.merge!(obj.response_headers)
-
resp.body = obj.body
-
resp.request = ActionDispatch::Request.new({})
-
end
-
::ActionDispatch::TestResponse.from_response(obj)
-
else
-
raise TypeError, "Invalid response type: #{obj}"
-
end
-
end
-
1
module_function :as_test_response
-
-
# @return [String, nil] a formatted failure message if
-
# `@invalid_response` is present, `nil` otherwise
-
1
def invalid_response_type_message
-
return unless @invalid_response
-
-
"expected a response object, but an instance of " \
-
"#{@invalid_response.class} was received"
-
end
-
-
# @api private
-
# Provides an implementation for `have_http_status` matching against
-
# numeric http status codes.
-
#
-
# Not intended to be instantiated directly.
-
#
-
# @example
-
# expect(response).to have_http_status(404)
-
#
-
# @see RSpec::Rails::Matchers#have_http_status
-
1
class NumericCode < RSpec::Rails::Matchers::BaseMatcher
-
1
include HaveHttpStatus
-
-
1
def initialize(code)
-
@expected = code.to_i
-
@actual = nil
-
@invalid_response = nil
-
end
-
-
# @param [Object] response object providing an http code to match
-
# @return [Boolean] `true` if the numeric code matched the `response` code
-
1
def matches?(response)
-
test_response = as_test_response(response)
-
@actual = test_response.response_code.to_i
-
expected == @actual
-
rescue TypeError => _ignored
-
@invalid_response = response
-
false
-
end
-
-
# @return [String]
-
1
def description
-
"respond with numeric status code #{expected}"
-
end
-
-
# @return [String] explaining why the match failed
-
1
def failure_message
-
invalid_response_type_message ||
-
"expected the response to have status code #{expected.inspect}" \
-
" but it was #{actual.inspect}"
-
end
-
-
# @return [String] explaining why the match failed
-
1
def failure_message_when_negated
-
invalid_response_type_message ||
-
"expected the response not to have status code " \
-
"#{expected.inspect} but it did"
-
end
-
end
-
-
# @api private
-
# Provides an implementation for `have_http_status` matching against
-
# Rack symbol http status codes.
-
#
-
# Not intended to be instantiated directly.
-
#
-
# @example
-
# expect(response).to have_http_status(:created)
-
#
-
# @see RSpec::Rails::Matchers#have_http_status
-
# @see https://github.com/rack/rack/blob/master/lib/rack/utils.rb `Rack::Utils::SYMBOL_TO_STATUS_CODE`
-
1
class SymbolicStatus < RSpec::Rails::Matchers::BaseMatcher
-
1
include HaveHttpStatus
-
-
1
def initialize(status)
-
@expected_status = status
-
@actual = nil
-
@invalid_response = nil
-
set_expected_code!
-
end
-
-
# @param [Object] response object providing an http code to match
-
# @return [Boolean] `true` if Rack's associated numeric HTTP code matched
-
# the `response` code
-
1
def matches?(response)
-
test_response = as_test_response(response)
-
@actual = test_response.response_code
-
expected == @actual
-
rescue TypeError => _ignored
-
@invalid_response = response
-
false
-
end
-
-
# @return [String]
-
1
def description
-
"respond with status code #{pp_expected}"
-
end
-
-
# @return [String] explaining why the match failed
-
1
def failure_message
-
invalid_response_type_message ||
-
"expected the response to have status code #{pp_expected} but it" \
-
" was #{pp_actual}"
-
end
-
-
# @return [String] explaining why the match failed
-
1
def failure_message_when_negated
-
invalid_response_type_message ||
-
"expected the response not to have status code #{pp_expected} " \
-
"but it did"
-
end
-
-
# The initialized expected status symbol
-
1
attr_reader :expected_status
-
1
private :expected_status
-
-
1
private
-
-
# @return [Symbol] representing the actual http numeric code
-
1
def actual_status
-
return unless actual
-
-
@actual_status ||= compute_status_from(actual)
-
end
-
-
# Reverse lookup of the Rack status code symbol based on the numeric
-
# http code
-
#
-
# @param code [Fixnum] http status code to look up
-
# @return [Symbol] representing the http numeric code
-
1
def compute_status_from(code)
-
status, _ = Rack::Utils::SYMBOL_TO_STATUS_CODE.find do |_, c|
-
c == code
-
end
-
status
-
end
-
-
# @return [String] pretty format the actual response status
-
1
def pp_actual
-
pp_status(actual_status, actual)
-
end
-
-
# @return [String] pretty format the expected status and associated code
-
1
def pp_expected
-
pp_status(expected_status, expected)
-
end
-
-
# @return [String] pretty format the actual response status
-
1
def pp_status(status, code)
-
if status
-
"#{status.inspect} (#{code})"
-
else
-
code.to_s
-
end
-
end
-
-
# Sets `expected` to the numeric http code based on the Rack
-
# `expected_status` status
-
#
-
# @see Rack::Utils::SYMBOL_TO_STATUS_CODE
-
# @raise [ArgumentError] if an associated code could not be found
-
1
def set_expected_code!
-
@expected ||=
-
Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(expected_status) do
-
raise ArgumentError,
-
"Invalid HTTP status: #{expected_status.inspect}"
-
end
-
end
-
end
-
-
# @api private
-
# Provides an implementation for `have_http_status` matching against
-
# `ActionDispatch::TestResponse` http status category queries.
-
#
-
# Not intended to be instantiated directly.
-
#
-
# @example
-
# expect(response).to have_http_status(:success)
-
# expect(response).to have_http_status(:error)
-
# expect(response).to have_http_status(:missing)
-
# expect(response).to have_http_status(:redirect)
-
#
-
# @see RSpec::Rails::Matchers#have_http_status
-
# @see https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
-
1
class GenericStatus < RSpec::Rails::Matchers::BaseMatcher
-
1
include HaveHttpStatus
-
-
# @return [Array<Symbol>] of status codes which represent a HTTP status
-
# code "group"
-
# @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
-
1
def self.valid_statuses
-
[
-
:error, :success, :missing,
-
:server_error, :successful, :not_found,
-
:redirect
-
]
-
end
-
-
1
def initialize(type)
-
unless self.class.valid_statuses.include?(type)
-
raise ArgumentError, "Invalid generic HTTP status: #{type.inspect}"
-
end
-
-
@expected = type
-
@actual = nil
-
@invalid_response = nil
-
end
-
-
# @return [Boolean] `true` if Rack's associated numeric HTTP code matched
-
# the `response` code or the named response status
-
1
def matches?(response)
-
test_response = as_test_response(response)
-
@actual = test_response.response_code
-
check_expected_status(test_response, expected)
-
rescue TypeError => _ignored
-
@invalid_response = response
-
false
-
end
-
-
# @return [String]
-
1
def description
-
"respond with #{type_message}"
-
end
-
-
# @return [String] explaining why the match failed
-
1
def failure_message
-
invalid_response_type_message ||
-
"expected the response to have #{type_message} but it was #{actual}"
-
end
-
-
# @return [String] explaining why the match failed
-
1
def failure_message_when_negated
-
invalid_response_type_message ||
-
"expected the response not to have #{type_message} but it was #{actual}"
-
end
-
-
1
protected
-
-
RESPONSE_METHODS = {
-
1
success: 'successful',
-
error: 'server_error',
-
missing: 'not_found'
-
}.freeze
-
-
1
def check_expected_status(test_response, expected)
-
test_response.send(
-
"#{RESPONSE_METHODS.fetch(expected, expected)}?")
-
end
-
-
1
private
-
-
# @return [String] formating the expected status and associated code(s)
-
1
def type_message
-
@type_message ||= (expected == :error ? "an error" : "a #{expected}") +
-
" status code (#{type_codes})"
-
end
-
-
# @return [String] formatting the associated code(s) for the various
-
# status code "groups"
-
# @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
-
# @see https://github.com/rack/rack/blob/master/lib/rack/response.rb `Rack::Response`
-
1
def type_codes
-
# At the time of this commit the most recent version of
-
# `ActionDispatch::TestResponse` defines the following aliases:
-
#
-
# alias_method :success?, :successful?
-
# alias_method :missing?, :not_found?
-
# alias_method :redirect?, :redirection?
-
# alias_method :error?, :server_error?
-
#
-
# It's parent `ActionDispatch::Response` includes
-
# `Rack::Response::Helpers` which defines the aliased methods as:
-
#
-
# def successful?; status >= 200 && status < 300; end
-
# def redirection?; status >= 300 && status < 400; end
-
# def server_error?; status >= 500 && status < 600; end
-
# def not_found?; status == 404; end
-
#
-
# @see https://github.com/rails/rails/blob/ca200378/actionpack/lib/action_dispatch/testing/test_response.rb#L17-L27
-
# @see https://github.com/rails/rails/blob/ca200378/actionpack/lib/action_dispatch/http/response.rb#L74
-
# @see https://github.com/rack/rack/blob/ce4a3959/lib/rack/response.rb#L119-L122
-
@type_codes ||= case expected
-
when :error, :server_error
-
"5xx"
-
when :success, :successful
-
"2xx"
-
when :missing, :not_found
-
"404"
-
when :redirect
-
"3xx"
-
end
-
end
-
end
-
end
-
-
# @api public
-
# Passes if `response` has a matching HTTP status code.
-
#
-
# The following symbolic status codes are allowed:
-
#
-
# - `Rack::Utils::SYMBOL_TO_STATUS_CODE`
-
# - One of the defined `ActionDispatch::TestResponse` aliases:
-
# - `:error`
-
# - `:missing`
-
# - `:redirect`
-
# - `:success`
-
#
-
# @example Accepts numeric and symbol statuses
-
# expect(response).to have_http_status(404)
-
# expect(response).to have_http_status(:created)
-
# expect(response).to have_http_status(:success)
-
# expect(response).to have_http_status(:error)
-
# expect(response).to have_http_status(:missing)
-
# expect(response).to have_http_status(:redirect)
-
#
-
# @example Works with standard `response` objects and Capybara's `page`
-
# expect(response).to have_http_status(404)
-
# expect(page).to have_http_status(:created)
-
#
-
# @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
-
# @see https://github.com/rack/rack/blob/master/lib/rack/utils.rb `Rack::Utils::SYMBOL_TO_STATUS_CODE`
-
1
def have_http_status(target)
-
raise ArgumentError, "Invalid HTTP status: nil" unless target
-
-
HaveHttpStatus.matcher_for_status(target)
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Matcher for template rendering.
-
1
module RenderTemplate
-
# @private
-
1
class RenderTemplateMatcher < RSpec::Rails::Matchers::BaseMatcher
-
1
def initialize(scope, expected, message = nil)
-
@expected = Symbol === expected ? expected.to_s : expected
-
@message = message
-
@scope = scope
-
@redirect_is = nil
-
end
-
-
# @api private
-
1
def matches?(*)
-
match_check = match_unless_raises ActiveSupport::TestCase::Assertion do
-
@scope.assert_template expected, @message
-
end
-
check_redirect unless match_check
-
match_check
-
end
-
-
# Uses normalize_argument_to_redirection to find and format
-
# the redirect location. normalize_argument_to_redirection is private
-
# in ActionDispatch::Assertions::ResponseAssertions so we call it
-
# here using #send. This will keep the error message format consistent
-
# @api private
-
1
def check_redirect
-
response = @scope.response
-
return unless response.respond_to?(:redirect?) && response.redirect?
-
-
@redirect_is = @scope.send(:normalize_argument_to_redirection, response.location)
-
end
-
-
# @api private
-
1
def failure_message
-
if @redirect_is
-
rescued_exception.message[/(.*?)( but|$)/, 1] +
-
" but was a redirect to <#{@redirect_is}>"
-
else
-
rescued_exception.message
-
end
-
end
-
-
# @api private
-
1
def failure_message_when_negated
-
"expected not to render #{expected.inspect}, but did"
-
end
-
end
-
-
# Delegates to `assert_template`.
-
#
-
# @example
-
# expect(response).to have_rendered("new")
-
1
def have_rendered(options, message = nil)
-
RenderTemplateMatcher.new(self, options, message)
-
end
-
-
1
alias_method :render_template, :have_rendered
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Matcher for redirects.
-
1
module RedirectTo
-
# @private
-
1
class RedirectTo < RSpec::Rails::Matchers::BaseMatcher
-
1
def initialize(scope, expected)
-
@expected = expected
-
@scope = scope
-
end
-
-
1
def matches?(_)
-
match_unless_raises ActiveSupport::TestCase::Assertion do
-
@scope.assert_redirected_to(@expected)
-
end
-
end
-
-
1
def failure_message
-
rescued_exception.message
-
end
-
-
1
def failure_message_when_negated
-
"expected not to redirect to #{@expected.inspect}, but did"
-
end
-
end
-
-
# Delegates to `assert_redirected_to`.
-
#
-
# @example
-
# expect(response).to redirect_to(:action => "new")
-
1
def redirect_to(target)
-
RedirectTo.new(self, target)
-
end
-
end
-
end
-
end
-
end
-
1
if defined?(ActiveRecord::Relation) && defined?(RSpec::Matchers::BuiltIn::OperatorMatcher) # RSpec 4 removed OperatorMatcher
-
1
RSpec::Matchers::BuiltIn::OperatorMatcher.register(ActiveRecord::Relation, '=~', RSpec::Matchers::BuiltIn::ContainExactly)
-
end
-
1
module RSpec
-
1
module Rails
-
1
module Matchers
-
# Matchers to help with specs for routing code.
-
1
module RoutingMatchers
-
1
extend RSpec::Matchers::DSL
-
-
# @private
-
1
class RouteToMatcher < RSpec::Rails::Matchers::BaseMatcher
-
1
def initialize(scope, *expected)
-
@scope = scope
-
@expected = expected[1] || {}
-
if Hash === expected[0]
-
@expected.merge!(expected[0])
-
else
-
controller, action = expected[0].split('#')
-
@expected.merge!(controller: controller, action: action)
-
end
-
end
-
-
1
def matches?(verb_to_path_map)
-
@actual = verb_to_path_map
-
# assert_recognizes does not consider ActionController::RoutingError an
-
# assertion failure, so we have to capture that and Assertion here.
-
match_unless_raises ActiveSupport::TestCase::Assertion, ActionController::RoutingError do
-
path, query = *verb_to_path_map.values.first.split('?')
-
@scope.assert_recognizes(
-
@expected,
-
{method: verb_to_path_map.keys.first, path: path},
-
Rack::Utils.parse_nested_query(query)
-
)
-
end
-
end
-
-
1
def failure_message
-
rescued_exception.message
-
end
-
-
1
def failure_message_when_negated
-
"expected #{@actual.inspect} not to route to #{@expected.inspect}"
-
end
-
-
1
def description
-
"route #{@actual.inspect} to #{@expected.inspect}"
-
end
-
end
-
-
# Delegates to `assert_recognizes`. Supports short-hand controller/action
-
# declarations (e.g. `"controller#action"`).
-
#
-
# @example
-
#
-
# expect(get: "/things/special").to route_to(
-
# controller: "things",
-
# action: "special"
-
# )
-
#
-
# expect(get: "/things/special").to route_to("things#special")
-
#
-
# @see https://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes
-
1
def route_to(*expected)
-
RouteToMatcher.new(self, *expected)
-
end
-
-
# @private
-
1
class BeRoutableMatcher < RSpec::Rails::Matchers::BaseMatcher
-
1
def initialize(scope)
-
@scope = scope
-
end
-
-
1
def matches?(path)
-
@actual = path
-
match_unless_raises ActionController::RoutingError do
-
@routing_options = @scope.routes.recognize_path(
-
path.values.first, method: path.keys.first
-
)
-
end
-
end
-
-
1
def failure_message
-
"expected #{@actual.inspect} to be routable"
-
end
-
-
1
def failure_message_when_negated
-
"expected #{@actual.inspect} not to be routable, but it routes to #{@routing_options.inspect}"
-
end
-
-
1
def description
-
"be routable"
-
end
-
end
-
-
# Passes if the route expression is recognized by the Rails router based on
-
# the declarations in `config/routes.rb`. Delegates to
-
# `RouteSet#recognize_path`.
-
#
-
# @example You can use route helpers provided by rspec-rails.
-
# expect(get: "/a/path").to be_routable
-
# expect(post: "/another/path").to be_routable
-
# expect(put: "/yet/another/path").to be_routable
-
1
def be_routable
-
BeRoutableMatcher.new(self)
-
end
-
-
# Helpers for matching different route types.
-
1
module RouteHelpers
-
# @!method get
-
# @!method post
-
# @!method put
-
# @!method patch
-
# @!method delete
-
# @!method options
-
# @!method head
-
#
-
# Shorthand method for matching this type of route.
-
1
%w[get post put patch delete options head].each do |method|
-
7
define_method method do |path|
-
{method.to_sym => path}
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
begin
-
1
require 'capybara/rspec'
-
rescue LoadError
-
end
-
-
begin
-
1
require 'capybara/rails'
-
rescue LoadError
-
end
-
-
1
if defined?(Capybara)
-
RSpec.configure do |c|
-
if defined?(Capybara::DSL)
-
c.include Capybara::DSL, type: :feature
-
if defined?(ActionPack) && ActionPack::VERSION::STRING >= "5.1"
-
c.include Capybara::DSL, type: :system
-
end
-
end
-
-
if defined?(Capybara::RSpecMatchers)
-
c.include Capybara::RSpecMatchers, type: :view
-
c.include Capybara::RSpecMatchers, type: :helper
-
c.include Capybara::RSpecMatchers, type: :mailer
-
c.include Capybara::RSpecMatchers, type: :controller
-
c.include Capybara::RSpecMatchers, type: :feature
-
c.include Capybara::RSpecMatchers, type: :system
-
end
-
-
unless defined?(Capybara::RSpecMatchers) || defined?(Capybara::DSL)
-
c.include Capybara, type: :request
-
c.include Capybara, type: :controller
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# Helpers for making instance variables available to views.
-
1
module ViewAssigns
-
# Assigns a value to an instance variable in the scope of the
-
# view being rendered.
-
#
-
# @example
-
#
-
# assign(:widget, stub_model(Widget))
-
1
def assign(key, value)
-
_encapsulated_assigns[key] = value
-
end
-
-
# Compat-shim for AbstractController::Rendering#view_assigns
-
#
-
# _assigns was deprecated in favor of view_assigns after
-
# Rails-3.0.0 was released. Since we are not able to predict when
-
# the _assigns/view_assigns patch will be released (I thought it
-
# would have been in 3.0.1, but 3.0.1 bypassed this change for a
-
# security fix), this bit ensures that we do the right thing without
-
# knowing anything about the Rails version we are dealing with.
-
#
-
# Once that change _is_ released, this can be changed to something
-
# that checks for the Rails version when the module is being
-
# interpreted, as it was before commit dd0095.
-
1
def view_assigns
-
super.merge(_encapsulated_assigns)
-
rescue
-
_assigns
-
end
-
-
# @private
-
1
def _assigns
-
super.merge(_encapsulated_assigns)
-
end
-
-
1
private
-
-
1
def _encapsulated_assigns
-
@_encapsulated_assigns ||= {}
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# Builds paths for view specs using a particular route set.
-
1
class ViewPathBuilder
-
1
def initialize(route_set)
-
self.class.send(:include, route_set.url_helpers)
-
end
-
-
# Given a hash of parameters, build a view path, if possible.
-
# Returns nil if no path can be built from the given params.
-
#
-
# @example
-
# # path can be built because all required params are present in the hash
-
# view_path_builder = ViewPathBuilder.new(::Rails.application.routes)
-
# view_path_builder.path_for({ :controller => 'posts', :action => 'show', :id => '54' })
-
# # => "/post/54"
-
#
-
# @example
-
# # path cannot be built because the params are missing a required element (:id)
-
# view_path_builder.path_for({ :controller => 'posts', :action => 'delete' })
-
# # => ActionController::UrlGenerationError: No route matches {:action=>"delete", :controller=>"posts"}
-
1
def path_for(path_params)
-
url_for(path_params.merge(only_path: true))
-
rescue => e
-
e.message
-
end
-
end
-
end
-
end
-
1
require 'action_view/testing/resolvers'
-
-
1
module RSpec
-
1
module Rails
-
# @api public
-
# Helpers for optionally rendering views in controller specs.
-
1
module ViewRendering
-
1
extend ActiveSupport::Concern
-
-
# @!attribute [r]
-
# Returns the controller object instance under test.
-
1
attr_reader :controller
-
-
# @private
-
1
attr_writer :controller
-
1
private :controller=
-
-
# DSL methods
-
1
module ClassMethods
-
# @see RSpec::Rails::ControllerExampleGroup
-
1
def render_views(true_or_false = true)
-
@render_views = true_or_false
-
end
-
-
# @api private
-
1
def render_views?
-
return @render_views if defined?(@render_views)
-
-
if superclass.respond_to?(:render_views?)
-
superclass.render_views?
-
else
-
RSpec.configuration.render_views?
-
end
-
end
-
end
-
-
# @api private
-
1
def render_views?
-
self.class.render_views? || !controller.class.respond_to?(:view_paths)
-
end
-
-
# @private
-
1
class EmptyTemplateResolver
-
1
def self.build(path)
-
if path.is_a?(::ActionView::Resolver)
-
ResolverDecorator.new(path)
-
else
-
FileSystemResolver.new(path)
-
end
-
end
-
-
1
def self.nullify_template_rendering(templates)
-
templates.map do |template|
-
::ActionView::Template.new(
-
"",
-
template.identifier,
-
EmptyTemplateHandler,
-
virtual_path: template.virtual_path,
-
format: template_format(template),
-
locals: []
-
)
-
end
-
end
-
-
1
if ::Rails::VERSION::STRING >= '6'
-
1
def self.template_format(template)
-
template.format
-
end
-
else
-
def self.template_format(template)
-
template.formats
-
end
-
end
-
-
# Delegates all methods to the submitted resolver and for all methods
-
# that return a collection of `ActionView::Template` instances, return
-
# templates with modified source
-
#
-
# @private
-
1
class ResolverDecorator
-
1
def initialize(resolver)
-
@resolver = resolver
-
end
-
-
1
def method_missing(name, *args, &block)
-
result = @resolver.send(name, *args, &block)
-
nullify_templates(result)
-
end
-
-
1
private
-
-
1
def nullify_templates(collection)
-
return collection unless collection.is_a?(Enumerable)
-
return collection unless collection.all? { |element| element.is_a?(::ActionView::Template) }
-
-
EmptyTemplateResolver.nullify_template_rendering(collection)
-
end
-
end
-
-
# Delegates find_templates to the submitted path set and then returns
-
# templates with modified source
-
#
-
# @private
-
1
class FileSystemResolver < ::ActionView::FileSystemResolver
-
1
private
-
-
1
def find_templates(*args)
-
templates = super
-
EmptyTemplateResolver.nullify_template_rendering(templates)
-
end
-
end
-
end
-
-
# @private
-
1
class EmptyTemplateHandler
-
1
def self.call(_template, _source = nil)
-
::Rails.logger.info(" Template rendering was prevented by rspec-rails. Use `render_views` to verify rendered view contents if necessary.")
-
-
%("")
-
end
-
end
-
-
# Used to null out view rendering in controller specs.
-
#
-
# @private
-
1
module EmptyTemplates
-
1
def prepend_view_path(new_path)
-
lookup_context.view_paths.unshift(*_path_decorator(*new_path))
-
end
-
-
1
def append_view_path(new_path)
-
lookup_context.view_paths.push(*_path_decorator(*new_path))
-
end
-
-
1
private
-
-
1
def _path_decorator(*paths)
-
paths.map { |path| EmptyTemplateResolver.build(path) }
-
end
-
end
-
-
# @private
-
1
RESOLVER_CACHE = Hash.new do |hash, path|
-
hash[path] = EmptyTemplateResolver.build(path)
-
end
-
-
1
included do
-
before do
-
unless render_views?
-
@_original_path_set = controller.class.view_paths
-
path_set = @_original_path_set.map { |resolver| RESOLVER_CACHE[resolver] }
-
-
controller.class.view_paths = path_set
-
controller.extend(EmptyTemplates)
-
end
-
end
-
-
after do
-
controller.class.view_paths = @_original_path_set unless render_views?
-
end
-
end
-
end
-
end
-
end
-
1
module RSpec
-
1
module Rails
-
# Adds methods (generally to ActionView::TestCase::TestController).
-
# Intended for use in view specs.
-
1
module ViewSpecMethods
-
1
module_function
-
-
# Adds methods `extra_params=` and `extra_params` to the indicated class.
-
# When class is `::ActionView::TestCase::TestController`, these methods
-
# are exposed in view specs on the `controller` object.
-
1
def add_to(klass)
-
return if klass.method_defined?(:extra_params) && klass.method_defined?(:extra_params=)
-
-
klass.module_exec do
-
# Set any extra parameters that rendering a URL for this view
-
# would require.
-
#
-
# @example
-
#
-
# # In "spec/views/widgets/show.html.erb_spec.rb":
-
# before do
-
# widget = Widget.create!(:name => "slicer")
-
# controller.extra_params = { :id => widget.id }
-
# end
-
def extra_params=(hash)
-
@extra_params = hash
-
request.path =
-
ViewPathBuilder.new(::Rails.application.routes).path_for(
-
extra_params.merge(request.path_parameters)
-
)
-
end
-
-
# Use to read extra parameters that are set in the view spec.
-
#
-
# @example
-
#
-
# # After the before in the above example:
-
# controller.extra_params
-
# # => { :id => 4 }
-
def extra_params
-
@extra_params ||= {}
-
@extra_params.dup.freeze
-
end
-
end
-
end
-
-
# Removes methods `extra_params=` and `extra_params` from the indicated class.
-
1
def remove_from(klass)
-
klass.module_exec do
-
undef extra_params= if klass.method_defined?(:extra_params=)
-
undef extra_params if klass.method_defined?(:extra_params)
-
end
-
end
-
end
-
end
-
end